PLAYWRIGHT.md
11.2 KB
Playwright E2E 测试指南
✅ 配置完成
你的项目现在已经配置了 Playwright E2E 测试框架!
📦 已安装的包
{
"devDependencies": {
"@playwright/test": "^1.58.0"
}
}
已安装的浏览器:
- Chromium (Chrome for Testing)
- Chrome Headless Shell
📁 配置文件
| 文件 | 说明 |
|---|---|
playwright.config.js |
Playwright 主配置文件 |
e2e/example.spec.js |
示例 E2E 测试 |
test-results/ |
测试结果输出目录(.gitignore) |
playwright-report/ |
HTML 报告输出目录(.gitignore) |
🚀 快速开始
1. 运行所有测试
# 运行所有 E2E 测试
pnpm test:e2e
# 输出示例:
# Running 9 tests using 1 worker
#
# ✓ [chromium-mobile]: › example.spec.js:3:1 › 基础功能测试 › 首页加载成功 (2.5s)
# ✓ [chromium-mobile]: › example.spec.js:10:1 › 基础功能测试 › 导航功能正常 (1.8s)
# ...
#
# 9 passed (12.3s)
2. 查看测试报告
# 生成并打开 HTML 报告
pnpm test:e2e:report
# 报告会在浏览器中自动打开
3. 调试模式
# 调试模式(逐步执行)
pnpm test:e2e:debug
# 有头模式(可以看到浏览器操作)
pnpm test:e2e:headed
# UI 模式(可视化界面)
pnpm test:e2e:ui
📝 编写测试
基础测试结构
import { test, expect } from '@playwright/test'
test.describe('测试套件', () => {
test.beforeEach(async ({ page }) => {
// 每个测试前执行
await page.goto('/')
})
test('测试名称', async ({ page }) => {
// 测试步骤
await page.click('button')
await expect(page).toHaveURL('/dashboard')
})
})
常用操作
页面导航
// 访问 URL
await page.goto('/login')
// 等待导航完成
await page.waitForLoadState('networkidle')
// 等待特定 URL
await expect(page).toHaveURL(/\/dashboard/)
元素定位
// CSS 选择器
const button = page.locator('button.submit')
const input = page.locator('#username')
const element = page.locator('.class-name')
// 文本选择器
const link = page.locator('text=点击这里')
const title = page.locator('h1:has-text("欢迎")')
// XPath
const element = page.locator('//button[@type="submit"]')
交互操作
// 点击
await page.click('button')
// 输入
await page.fill('input[name="email"]', 'test@example.com')
// 选择下拉
await page.selectOption('select#country', 'China')
// 上传文件
await page.setInputFiles('input[type="file"]', 'file.pdf')
// 悬停
await page.hover('.menu-item')
# 滚动
await page.evaluate(() => window.scrollTo(0, 1000))
断言
// 元素可见性
await expect(page.locator('.header')).toBeVisible()
// 元素存在
await expect(page.locator('.error')).not.toBeVisible()
// 文本内容
await expect(page.locator('h1')).toHaveText('欢迎')
// 属性值
await expect(page.locator('input')).toHaveValue('test')
# URL
await expect(page).toHaveURL('/dashboard')
# 标题
await expect(page).toHaveTitle('首页')
高级用法
等待策略
// 等待元素出现
await page.waitForSelector('.loading', { state: 'hidden' })
// 等待特定时间
await page.waitForTimeout(1000)
// 等待网络响应
await page.waitForResponse('**/api/user')
// 等待函数条件
await page.waitForFunction(() => {
return document.querySelector('.button').disabled === false
})
处理弹窗
// 处理 alert
page.on('dialog', dialog => {
console.log(dialog.message())
dialog.accept()
})
await page.click('button#alert')
// 处理 confirm
page.on('dialog', dialog => {
dialog.dismiss() // 取消
})
await page.click('button#confirm')
截图和录制
// 截图
await page.screenshot({ path: 'screenshot.png' })
// 元素截图
await page.locator('.header').screenshot({ path: 'header.png' })
// 全页截图
await page.screenshot({ path: 'full.png', fullPage: true })
API 请求
// 拦截请求
page.route('**/api/user', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ name: 'Test' })
})
})
// 监听响应
page.on('response', response => {
if (response.url().includes('api/user')) {
console.log(await response.json())
}
})
📋 测试最佳实践
1. 测试命名
// ✓ GOOD - 清晰的描述
test('用户登录后应跳转到首页', async ({ page }) => {})
// ✗ BAD - 不明确的名称
test('test1', async ({ page }) => {})
2. 选择器优先级
// 优先级:data-testid > aria-label > 文本 > CSS 选择器
// ✓ BEST - 使用 data-testid
await page.click('[data-testid="submit-button"]')
// ✓ GOOD - 使用 aria-label
await page.click('button[aria-label="提交"]')
// ✓ OK - 使用文本
await page.click('text=提交')
// ✗ BAD - 脆弱的 CSS 选择器
await page.click('div.container > div:nth-child(3) > button')
3. Page Object Model
// pages/LoginPage.js
class LoginPage {
constructor(page) {
this.page = page
this.usernameInput = page.locator('input[name="username"]')
this.passwordInput = page.locator('input[name="password"]')
this.submitButton = page.locator('button[type="submit"]')
}
async login(username, password) {
await this.usernameInput.fill(username)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
// 测试中使用
test('用户登录', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.login('test@example.com', 'password')
await expect(page).toHaveURL('/dashboard')
})
4. 测试数据管理
// 使用 fixtures 管理测试数据
const testUsers = {
validUser: {
email: 'test@example.com',
password: 'password123',
},
invalidUser: {
email: 'invalid@example.com',
password: 'wrongpassword',
},
}
test('登录成功', async ({ page }) => {
await login(page, testUsers.validUser)
await expect(page).toHaveURL('/dashboard')
})
🎯 测试场景示例
1. 完整的用户流程
test('新用户注册流程', async ({ page }) => {
// 1. 访问注册页面
await page.goto('/register')
// 2. 填写表单
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('input[name="password"]', 'password123')
await page.fill('input[name="confirm"]', 'password123')
// 3. 提交表单
await page.click('button[type="submit"]')
// 4. 验证跳转
await expect(page).toHaveURL('/welcome')
// 5. 验证提示信息
const toast = page.locator('.van-toast--success')
await expect(toast).toBeVisible()
})
2. 表单验证
test('登录表单验证', async ({ page }) => {
await page.goto('/login')
// 不输入任何信息,直接提交
await page.click('button[type="submit"]')
// 验证错误提示
const errorToast = page.locator('.van-toast--fail')
await expect(errorToast).toBeVisible()
// 输入无效邮箱
await page.fill('input[name="email"]', 'invalid-email')
await page.click('button[type="submit"]')
// 验证错误提示
await expect(errorToast).toContainText('邮箱格式不正确')
})
3. 响应式测试
test.describe('响应式布局', () => {
test('移动端显示底部导航', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/')
const bottomNav = page.locator('.van-tabbar')
await expect(bottomNav).toBeVisible()
})
test('桌面端显示侧边栏', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 720 })
await page.goto('/')
const sidebar = page.locator('.sidebar')
await expect(sidebar).toBeVisible()
})
})
🔧 调试技巧
1. 使用 Playwright Inspector
# 启动 Inspector 模式
pnpm test:e2e:debug
# 在代码中添加断点
await page.pause() // 暂停执行,打开 Inspector
2. 查看执行过程
# 有头模式(可以看到浏览器操作)
pnpm test:e2e --headed
# 慢动作模式
pnpm test:e2e --slow-mo=1000 # 每个操作延迟 1 秒
3. 详细日志
// 在测试中启用调试
test('调试示例', async ({ page }) => {
// 启用详细日志
page.on('console', msg => console.log(msg.text()))
page.on('pageerror', err => console.log(err))
await page.goto('/')
})
📊 测试报告
HTML 报告
# 生成 HTML 报告
pnpm test:e2e
# 查看报告
pnpm test:e2e:report
报告功能:
- 查看所有测试结果
- 查看截图和视频
- 查看时间线
- 追踪错误
JSON 报告
# JSON 报告会自动生成
cat test-results/test-results.json
JUnit 报告
# JUnit 报告用于 CI 集成
cat test-results/test-results.xml
🎭 配置选项
测试项目(projects)
playwright.config.js 中配置了 3 个测试项目:
projects: [
{
name: 'chromium-mobile',
use: { ...devices['iPhone 12'] },
},
{
name: 'chromium-desktop',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'webkit-mobile',
use: { ...devices['iPhone 12'], browserName: 'webkit' },
},
]
运行特定项目
# 只运行移动端测试
pnpm test:e2e --project=chromium-mobile
# 只运行桌面端测试
pnpm test:e2e --project=chromium-desktop
失败重试
// CI 环境自动重试 2 次
retries: process.env.CI ? 2 : 0
超时配置
// 测试超时 30 秒
timeout: 30 * 1000
// 断言超时 5 秒
expect: {
timeout: 5 * 1000
}
// 操作超时 10 秒
use: {
actionTimeout: 10 * 1000
}
🚀 CI/CD 集成
GitHub Actions
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Install dependencies
run: pnpm install
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload test report
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
📚 参考资源
🎉 总结
Playwright 优势:
- ✅ 跨浏览器支持(Chromium、Firefox、WebKit)
- ✅ 跨平台支持(Windows、macOS、Linux)
- ✅ 快速可靠(并行执行、自动等待)
- ✅ 强大的调试工具(Inspector、Trace、Video)
- ✅ 完整的测试报告
- ✅ 移动端支持
下一步:
- 编写核心功能的 E2E 测试
- 集成到 CI/CD 流程
- 定期更新测试用例
享受高质量的 E2E 测试!🚀