Playwright Page Object 模式 #
Page Object 模式概述 #
Page Object 模式(POM)是一种设计模式,将页面元素和操作封装成对象,使测试代码更加清晰、可维护。
优势 #
text
┌─────────────────────────────────────────────────────────────┐
│ Page Object 模式优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 代码复用 - 页面操作封装,多处使用 │
│ ✅ 易于维护 - 元素变化只需修改一处 │
│ ✅ 可读性强 - 测试代码描述业务逻辑 │
│ ✅ 解耦合 - 测试与页面实现分离 │
│ ✅ 团队协作 - 分工明确,互不干扰 │
│ │
└─────────────────────────────────────────────────────────────┘
项目结构 #
text
project/
├── pages/
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ └── ProductPage.ts
├── tests/
│ ├── login.spec.ts
│ ├── dashboard.spec.ts
│ └── product.spec.ts
└── playwright.config.ts
基础 Page Object #
创建 Page Object 类 #
typescript
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Sign In' });
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectErrorMessage(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
}
使用 Page Object #
typescript
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('登录测试', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
test('登录失败测试', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('wrong@example.com', 'wrongpassword');
await loginPage.expectErrorMessage('Invalid credentials');
});
基类设计 #
创建 BasePage #
typescript
// pages/BasePage.ts
import { Page, Locator, expect } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
// 导航方法
async goto(path: string) {
await this.page.goto(path);
}
// 等待页面加载
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
// 截图
async screenshot(name: string) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
// 等待元素
async waitForElement(locator: Locator) {
await locator.waitFor({ state: 'visible' });
}
// 点击并等待导航
async clickAndWaitForNavigation(locator: Locator) {
await Promise.all([
this.page.waitForNavigation(),
locator.click(),
]);
}
// 获取元素文本
async getElementText(locator: Locator): Promise<string | null> {
return await locator.textContent();
}
// 检查元素是否存在
async isElementVisible(locator: Locator): Promise<boolean> {
return await locator.isVisible();
}
}
继承 BasePage #
typescript
// pages/DashboardPage.ts
import { Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class DashboardPage extends BasePage {
readonly welcomeMessage: Locator;
readonly userMenu: Locator;
readonly logoutButton: Locator;
readonly notifications: Locator;
constructor(page: Page) {
super(page);
this.welcomeMessage = page.locator('.welcome-message');
this.userMenu = page.getByRole('button', { name: 'User Menu' });
this.logoutButton = page.getByRole('button', { name: 'Logout' });
this.notifications = page.locator('.notification-item');
}
async goto() {
await super.goto('/dashboard');
await this.waitForPageLoad();
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
}
async expectWelcomeMessage(userName: string) {
await expect(this.welcomeMessage).toContainText(`Welcome, ${userName}`);
}
async getNotificationCount(): Promise<number> {
return await this.notifications.count();
}
}
复杂 Page Object #
带组件的 Page Object #
typescript
// pages/components/Navigation.ts
import { Page, Locator } from '@playwright/test';
export class Navigation {
readonly page: Page;
readonly homeLink: Locator;
readonly productsLink: Locator;
readonly cartLink: Locator;
readonly profileLink: Locator;
constructor(page: Page) {
this.page = page;
this.homeLink = page.getByRole('link', { name: 'Home' });
this.productsLink = page.getByRole('link', { name: 'Products' });
this.cartLink = page.getByRole('link', { name: 'Cart' });
this.profileLink = page.getByRole('link', { name: 'Profile' });
}
async goToHome() {
await this.homeLink.click();
}
async goToProducts() {
await this.productsLink.click();
}
async goToCart() {
await this.cartLink.click();
}
async goToProfile() {
await this.profileLink.click();
}
}
// pages/ProductPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
import { Navigation } from './components/Navigation';
export class ProductPage extends BasePage {
readonly navigation: Navigation;
readonly productCards: Locator;
readonly searchInput: Locator;
readonly searchButton: Locator;
readonly filterDropdown: Locator;
constructor(page: Page) {
super(page);
this.navigation = new Navigation(page);
this.productCards = page.locator('.product-card');
this.searchInput = page.getByPlaceholder('Search products');
this.searchButton = page.getByRole('button', { name: 'Search' });
this.filterDropdown = page.getByRole('combobox', { name: 'Filter' });
}
async goto() {
await super.goto('/products');
}
async searchProduct(query: string) {
await this.searchInput.fill(query);
await this.searchButton.click();
await this.page.waitForLoadState('networkidle');
}
async filterByCategory(category: string) {
await this.filterDropdown.selectOption(category);
await this.page.waitForLoadState('networkidle');
}
async getProductCount(): Promise<number> {
return await this.productCards.count();
}
async selectProduct(index: number) {
await this.productCards.nth(index).click();
}
async expectProductCount(count: number) {
await expect(this.productCards).toHaveCount(count);
}
}
使用组件 #
typescript
// tests/product.spec.ts
import { test, expect } from '@playwright/test';
import { ProductPage } from '../pages/ProductPage';
test('产品搜索测试', async ({ page }) => {
const productPage = new ProductPage(page);
await productPage.goto();
await productPage.searchProduct('iPhone');
await productPage.expectProductCount(5);
});
test('导航测试', async ({ page }) => {
const productPage = new ProductPage(page);
await productPage.goto();
await productPage.navigation.goToCart();
await expect(page).toHaveURL('/cart');
});
Page Object 与 Fixtures 结合 #
创建 Page Object Fixtures #
typescript
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
import { ProductPage } from './pages/ProductPage';
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
productPage: ProductPage;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
productPage: async ({ page }, use) => {
const productPage = new ProductPage(page);
await use(productPage);
},
});
export { expect } from '@playwright/test';
使用 Fixtures #
typescript
// tests/dashboard.spec.ts
import { test, expect } from '../fixtures';
test('仪表板测试', async ({ loginPage, dashboardPage }) => {
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(dashboardPage.welcomeMessage).toBeVisible();
});
test('产品页面测试', async ({ productPage }) => {
await productPage.goto();
await productPage.searchProduct('Laptop');
await expect(productPage.productCards.first()).toBeVisible();
});
认证状态复用 #
保存认证状态 #
typescript
// auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
const authFile = 'playwright/.auth/user.json';
setup('认证', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
// 保存认证状态
await page.context().storageState({ path: authFile });
});
使用认证状态 #
typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// 认证设置项目
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
// 使用认证状态的项目
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
需要/不需要认证的 Page Object #
typescript
// pages/SecurePage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class SecurePage extends BasePage {
readonly profileSection: Locator;
readonly settingsLink: Locator;
constructor(page: Page) {
super(page);
this.profileSection = page.locator('.profile-section');
this.settingsLink = page.getByRole('link', { name: 'Settings' });
}
async goto() {
await super.goto('/secure/dashboard');
// 假设已经通过 storageState 认证
}
async expectAuthenticated() {
await expect(this.profileSection).toBeVisible();
}
}
数据驱动的 Page Object #
表单 Page Object #
typescript
// pages/RegistrationPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
interface RegistrationData {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
}
export class RegistrationPage extends BasePage {
readonly firstNameInput: Locator;
readonly lastNameInput: Locator;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly confirmPasswordInput: Locator;
readonly submitButton: Locator;
readonly successMessage: Locator;
constructor(page: Page) {
super(page);
this.firstNameInput = page.getByLabel('First Name');
this.lastNameInput = page.getByLabel('Last Name');
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.confirmPasswordInput = page.getByLabel('Confirm Password');
this.submitButton = page.getByRole('button', { name: 'Register' });
this.successMessage = page.locator('.success-message');
}
async goto() {
await super.goto('/register');
}
async fillForm(data: RegistrationData) {
await this.firstNameInput.fill(data.firstName);
await this.lastNameInput.fill(data.lastName);
await this.emailInput.fill(data.email);
await this.passwordInput.fill(data.password);
await this.confirmPasswordInput.fill(data.confirmPassword);
}
async submit() {
await this.submitButton.click();
}
async register(data: RegistrationData) {
await this.fillForm(data);
await this.submit();
}
async expectSuccess() {
await expect(this.successMessage).toBeVisible();
}
}
使用数据驱动 #
typescript
// tests/registration.spec.ts
import { test, expect } from '@playwright/test';
import { RegistrationPage } from '../pages/RegistrationPage';
const testCases = [
{
name: '有效注册数据',
data: {
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
password: 'Password123!',
confirmPassword: 'Password123!',
},
expectedSuccess: true,
},
{
name: '密码不匹配',
data: {
firstName: 'Jane',
lastName: 'Doe',
email: 'jane@example.com',
password: 'Password123!',
confirmPassword: 'DifferentPassword',
},
expectedSuccess: false,
},
];
for (const { name, data, expectedSuccess } of testCases) {
test(name, async ({ page }) => {
const registrationPage = new RegistrationPage(page);
await registrationPage.goto();
await registrationPage.register(data);
if (expectedSuccess) {
await registrationPage.expectSuccess();
}
});
}
最佳实践 #
1. 单一职责原则 #
typescript
// ✅ 推荐 - 每个方法只做一件事
async fillEmail(email: string) {
await this.emailInput.fill(email);
}
async fillPassword(password: string) {
await this.passwordInput.fill(password);
}
async submit() {
await this.submitButton.click();
}
// 组合方法
async login(email: string, password: string) {
await this.fillEmail(email);
await this.fillPassword(password);
await this.submit();
}
2. 使用语义化 Locator #
typescript
// ✅ 推荐
constructor(page: Page) {
this.emailInput = page.getByLabel('Email');
this.submitButton = page.getByRole('button', { name: 'Submit' });
}
// ❌ 不推荐
constructor(page: Page) {
this.emailInput = page.locator('#email-input');
this.submitButton = page.locator('.btn-submit');
}
3. 封装断言 #
typescript
// ✅ 推荐 - 在 Page Object 中封装断言
async expectLoginSuccess() {
await expect(this.page).toHaveURL('/dashboard');
await expect(this.welcomeMessage).toBeVisible();
}
// 测试中使用
await loginPage.login('user@example.com', 'password');
await dashboardPage.expectLoginSuccess();
4. 处理动态内容 #
typescript
// ✅ 推荐 - 等待动态内容加载
async waitForProductsLoaded() {
await this.page.waitForResponse('**/api/products');
await expect(this.productCards.first()).toBeVisible();
}
5. 使用 TypeScript 类型 #
typescript
// ✅ 推荐 - 定义接口
interface Product {
name: string;
price: number;
quantity: number;
}
async addToCart(product: Product) {
await this.searchProduct(product.name);
await this.selectProduct(0);
await this.setQuantity(product.quantity);
await this.clickAddToCart();
}
下一步 #
现在你已经掌握了 Page Object 模式,接下来学习 测试夹具 了解更多代码组织技巧!
最后更新:2026-03-28