chore: 添加完整的代码质量和测试工具链
## 新增工具 ### 1. ESLint + Prettier - ESLint 9.39.2 - 代码质量检查(Vue 3 规则) - Prettier 3.8.1 - 代码格式化 - prettier-plugin-tailwindcss - TailwindCSS 类名排序 - 配置文件:eslint.config.js, .prettierrc - VS Code 自动格式化配置 ### 2. Husky + lint-staged - Husky 9.1.7 - Git hooks 管理 - lint-staged 16.2.7 - 暂存文件检查 - pre-commit hook 自动运行 lint 和 format - 提交前自动修复代码问题 ### 3. Playwright E2E 测试 - @playwright/test 1.58.0 - E2E 测试框架 - 已安装 Chromium 和 Chrome Headless Shell - 配置文件:playwright.config.js - 示例测试:e2e/example.spec.js - 支持 3 个测试项目(移动端、桌面端、Safari) ## 配置更新 - package.json:添加 lint、format、test:e2e 等脚本 - eslint.config.js:支持 Vue、Vitest、Playwright - .gitignore:忽略 Playwright 测试结果 ## 新增文档 - docs/ESLINT_PRETTIER.md - ESLint 和 Prettier 使用指南 - docs/HUSKY_LINT_STAGED.md - Git Hooks 配置说明 - docs/PLAYWRIGHT.md - E2E 测试完整指南 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
6 changed files
with
803 additions
and
1 deletions
docs/PLAYWRIGHT.md
0 → 100644
| 1 | +# Playwright E2E 测试指南 | ||
| 2 | + | ||
| 3 | +## ✅ 配置完成 | ||
| 4 | + | ||
| 5 | +你的项目现在已经配置了 **Playwright** E2E 测试框架! | ||
| 6 | + | ||
| 7 | +## 📦 已安装的包 | ||
| 8 | + | ||
| 9 | +```json | ||
| 10 | +{ | ||
| 11 | + "devDependencies": { | ||
| 12 | + "@playwright/test": "^1.58.0" | ||
| 13 | + } | ||
| 14 | +} | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +**已安装的浏览器**: | ||
| 18 | + | ||
| 19 | +- Chromium (Chrome for Testing) | ||
| 20 | +- Chrome Headless Shell | ||
| 21 | + | ||
| 22 | +## 📁 配置文件 | ||
| 23 | + | ||
| 24 | +| 文件 | 说明 | | ||
| 25 | +| ---------------------- | ------------------------------- | | ||
| 26 | +| `playwright.config.js` | Playwright 主配置文件 | | ||
| 27 | +| `e2e/example.spec.js` | 示例 E2E 测试 | | ||
| 28 | +| `test-results/` | 测试结果输出目录(.gitignore) | | ||
| 29 | +| `playwright-report/` | HTML 报告输出目录(.gitignore) | | ||
| 30 | + | ||
| 31 | +## 🚀 快速开始 | ||
| 32 | + | ||
| 33 | +### 1. 运行所有测试 | ||
| 34 | + | ||
| 35 | +```bash | ||
| 36 | +# 运行所有 E2E 测试 | ||
| 37 | +pnpm test:e2e | ||
| 38 | + | ||
| 39 | +# 输出示例: | ||
| 40 | +# Running 9 tests using 1 worker | ||
| 41 | +# | ||
| 42 | +# ✓ [chromium-mobile]: › example.spec.js:3:1 › 基础功能测试 › 首页加载成功 (2.5s) | ||
| 43 | +# ✓ [chromium-mobile]: › example.spec.js:10:1 › 基础功能测试 › 导航功能正常 (1.8s) | ||
| 44 | +# ... | ||
| 45 | +# | ||
| 46 | +# 9 passed (12.3s) | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +### 2. 查看测试报告 | ||
| 50 | + | ||
| 51 | +```bash | ||
| 52 | +# 生成并打开 HTML 报告 | ||
| 53 | +pnpm test:e2e:report | ||
| 54 | + | ||
| 55 | +# 报告会在浏览器中自动打开 | ||
| 56 | +``` | ||
| 57 | + | ||
| 58 | +### 3. 调试模式 | ||
| 59 | + | ||
| 60 | +```bash | ||
| 61 | +# 调试模式(逐步执行) | ||
| 62 | +pnpm test:e2e:debug | ||
| 63 | + | ||
| 64 | +# 有头模式(可以看到浏览器操作) | ||
| 65 | +pnpm test:e2e:headed | ||
| 66 | + | ||
| 67 | +# UI 模式(可视化界面) | ||
| 68 | +pnpm test:e2e:ui | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +## 📝 编写测试 | ||
| 72 | + | ||
| 73 | +### 基础测试结构 | ||
| 74 | + | ||
| 75 | +```javascript | ||
| 76 | +import { test, expect } from '@playwright/test' | ||
| 77 | + | ||
| 78 | +test.describe('测试套件', () => { | ||
| 79 | + test.beforeEach(async ({ page }) => { | ||
| 80 | + // 每个测试前执行 | ||
| 81 | + await page.goto('/') | ||
| 82 | + }) | ||
| 83 | + | ||
| 84 | + test('测试名称', async ({ page }) => { | ||
| 85 | + // 测试步骤 | ||
| 86 | + await page.click('button') | ||
| 87 | + await expect(page).toHaveURL('/dashboard') | ||
| 88 | + }) | ||
| 89 | +}) | ||
| 90 | +``` | ||
| 91 | + | ||
| 92 | +### 常用操作 | ||
| 93 | + | ||
| 94 | +#### 页面导航 | ||
| 95 | + | ||
| 96 | +```javascript | ||
| 97 | +// 访问 URL | ||
| 98 | +await page.goto('/login') | ||
| 99 | + | ||
| 100 | +// 等待导航完成 | ||
| 101 | +await page.waitForLoadState('networkidle') | ||
| 102 | + | ||
| 103 | +// 等待特定 URL | ||
| 104 | +await expect(page).toHaveURL(/\/dashboard/) | ||
| 105 | +``` | ||
| 106 | + | ||
| 107 | +#### 元素定位 | ||
| 108 | + | ||
| 109 | +```javascript | ||
| 110 | +// CSS 选择器 | ||
| 111 | +const button = page.locator('button.submit') | ||
| 112 | +const input = page.locator('#username') | ||
| 113 | +const element = page.locator('.class-name') | ||
| 114 | + | ||
| 115 | +// 文本选择器 | ||
| 116 | +const link = page.locator('text=点击这里') | ||
| 117 | +const title = page.locator('h1:has-text("欢迎")') | ||
| 118 | + | ||
| 119 | +// XPath | ||
| 120 | +const element = page.locator('//button[@type="submit"]') | ||
| 121 | +``` | ||
| 122 | + | ||
| 123 | +#### 交互操作 | ||
| 124 | + | ||
| 125 | +```javascript | ||
| 126 | +// 点击 | ||
| 127 | +await page.click('button') | ||
| 128 | + | ||
| 129 | +// 输入 | ||
| 130 | +await page.fill('input[name="email"]', 'test@example.com') | ||
| 131 | + | ||
| 132 | +// 选择下拉 | ||
| 133 | +await page.selectOption('select#country', 'China') | ||
| 134 | + | ||
| 135 | +// 上传文件 | ||
| 136 | +await page.setInputFiles('input[type="file"]', 'file.pdf') | ||
| 137 | + | ||
| 138 | +// 悬停 | ||
| 139 | +await page.hover('.menu-item') | ||
| 140 | + | ||
| 141 | +# 滚动 | ||
| 142 | +await page.evaluate(() => window.scrollTo(0, 1000)) | ||
| 143 | +``` | ||
| 144 | + | ||
| 145 | +#### 断言 | ||
| 146 | + | ||
| 147 | +```javascript | ||
| 148 | +// 元素可见性 | ||
| 149 | +await expect(page.locator('.header')).toBeVisible() | ||
| 150 | + | ||
| 151 | +// 元素存在 | ||
| 152 | +await expect(page.locator('.error')).not.toBeVisible() | ||
| 153 | + | ||
| 154 | +// 文本内容 | ||
| 155 | +await expect(page.locator('h1')).toHaveText('欢迎') | ||
| 156 | + | ||
| 157 | +// 属性值 | ||
| 158 | +await expect(page.locator('input')).toHaveValue('test') | ||
| 159 | + | ||
| 160 | +# URL | ||
| 161 | +await expect(page).toHaveURL('/dashboard') | ||
| 162 | + | ||
| 163 | +# 标题 | ||
| 164 | +await expect(page).toHaveTitle('首页') | ||
| 165 | +``` | ||
| 166 | + | ||
| 167 | +### 高级用法 | ||
| 168 | + | ||
| 169 | +#### 等待策略 | ||
| 170 | + | ||
| 171 | +```javascript | ||
| 172 | +// 等待元素出现 | ||
| 173 | +await page.waitForSelector('.loading', { state: 'hidden' }) | ||
| 174 | + | ||
| 175 | +// 等待特定时间 | ||
| 176 | +await page.waitForTimeout(1000) | ||
| 177 | + | ||
| 178 | +// 等待网络响应 | ||
| 179 | +await page.waitForResponse('**/api/user') | ||
| 180 | + | ||
| 181 | +// 等待函数条件 | ||
| 182 | +await page.waitForFunction(() => { | ||
| 183 | + return document.querySelector('.button').disabled === false | ||
| 184 | +}) | ||
| 185 | +``` | ||
| 186 | + | ||
| 187 | +#### 处理弹窗 | ||
| 188 | + | ||
| 189 | +```javascript | ||
| 190 | +// 处理 alert | ||
| 191 | +page.on('dialog', dialog => { | ||
| 192 | + console.log(dialog.message()) | ||
| 193 | + dialog.accept() | ||
| 194 | +}) | ||
| 195 | +await page.click('button#alert') | ||
| 196 | + | ||
| 197 | +// 处理 confirm | ||
| 198 | +page.on('dialog', dialog => { | ||
| 199 | + dialog.dismiss() // 取消 | ||
| 200 | +}) | ||
| 201 | +await page.click('button#confirm') | ||
| 202 | +``` | ||
| 203 | + | ||
| 204 | +#### 截图和录制 | ||
| 205 | + | ||
| 206 | +```javascript | ||
| 207 | +// 截图 | ||
| 208 | +await page.screenshot({ path: 'screenshot.png' }) | ||
| 209 | + | ||
| 210 | +// 元素截图 | ||
| 211 | +await page.locator('.header').screenshot({ path: 'header.png' }) | ||
| 212 | + | ||
| 213 | +// 全页截图 | ||
| 214 | +await page.screenshot({ path: 'full.png', fullPage: true }) | ||
| 215 | +``` | ||
| 216 | + | ||
| 217 | +#### API 请求 | ||
| 218 | + | ||
| 219 | +```javascript | ||
| 220 | +// 拦截请求 | ||
| 221 | +page.route('**/api/user', route => { | ||
| 222 | + route.fulfill({ | ||
| 223 | + status: 200, | ||
| 224 | + body: JSON.stringify({ name: 'Test' }) | ||
| 225 | + }) | ||
| 226 | +}) | ||
| 227 | + | ||
| 228 | +// 监听响应 | ||
| 229 | +page.on('response', response => { | ||
| 230 | + if (response.url().includes('api/user')) { | ||
| 231 | + console.log(await response.json()) | ||
| 232 | + } | ||
| 233 | +}) | ||
| 234 | +``` | ||
| 235 | + | ||
| 236 | +## 📋 测试最佳实践 | ||
| 237 | + | ||
| 238 | +### 1. 测试命名 | ||
| 239 | + | ||
| 240 | +```javascript | ||
| 241 | +// ✓ GOOD - 清晰的描述 | ||
| 242 | +test('用户登录后应跳转到首页', async ({ page }) => {}) | ||
| 243 | + | ||
| 244 | +// ✗ BAD - 不明确的名称 | ||
| 245 | +test('test1', async ({ page }) => {}) | ||
| 246 | +``` | ||
| 247 | + | ||
| 248 | +### 2. 选择器优先级 | ||
| 249 | + | ||
| 250 | +```javascript | ||
| 251 | +// 优先级:data-testid > aria-label > 文本 > CSS 选择器 | ||
| 252 | + | ||
| 253 | +// ✓ BEST - 使用 data-testid | ||
| 254 | +await page.click('[data-testid="submit-button"]') | ||
| 255 | + | ||
| 256 | +// ✓ GOOD - 使用 aria-label | ||
| 257 | +await page.click('button[aria-label="提交"]') | ||
| 258 | + | ||
| 259 | +// ✓ OK - 使用文本 | ||
| 260 | +await page.click('text=提交') | ||
| 261 | + | ||
| 262 | +// ✗ BAD - 脆弱的 CSS 选择器 | ||
| 263 | +await page.click('div.container > div:nth-child(3) > button') | ||
| 264 | +``` | ||
| 265 | + | ||
| 266 | +### 3. Page Object Model | ||
| 267 | + | ||
| 268 | +```javascript | ||
| 269 | +// pages/LoginPage.js | ||
| 270 | +class LoginPage { | ||
| 271 | + constructor(page) { | ||
| 272 | + this.page = page | ||
| 273 | + this.usernameInput = page.locator('input[name="username"]') | ||
| 274 | + this.passwordInput = page.locator('input[name="password"]') | ||
| 275 | + this.submitButton = page.locator('button[type="submit"]') | ||
| 276 | + } | ||
| 277 | + | ||
| 278 | + async login(username, password) { | ||
| 279 | + await this.usernameInput.fill(username) | ||
| 280 | + await this.passwordInput.fill(password) | ||
| 281 | + await this.submitButton.click() | ||
| 282 | + } | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +// 测试中使用 | ||
| 286 | +test('用户登录', async ({ page }) => { | ||
| 287 | + const loginPage = new LoginPage(page) | ||
| 288 | + await loginPage.login('test@example.com', 'password') | ||
| 289 | + await expect(page).toHaveURL('/dashboard') | ||
| 290 | +}) | ||
| 291 | +``` | ||
| 292 | + | ||
| 293 | +### 4. 测试数据管理 | ||
| 294 | + | ||
| 295 | +```javascript | ||
| 296 | +// 使用 fixtures 管理测试数据 | ||
| 297 | +const testUsers = { | ||
| 298 | + validUser: { | ||
| 299 | + email: 'test@example.com', | ||
| 300 | + password: 'password123', | ||
| 301 | + }, | ||
| 302 | + invalidUser: { | ||
| 303 | + email: 'invalid@example.com', | ||
| 304 | + password: 'wrongpassword', | ||
| 305 | + }, | ||
| 306 | +} | ||
| 307 | + | ||
| 308 | +test('登录成功', async ({ page }) => { | ||
| 309 | + await login(page, testUsers.validUser) | ||
| 310 | + await expect(page).toHaveURL('/dashboard') | ||
| 311 | +}) | ||
| 312 | +``` | ||
| 313 | + | ||
| 314 | +## 🎯 测试场景示例 | ||
| 315 | + | ||
| 316 | +### 1. 完整的用户流程 | ||
| 317 | + | ||
| 318 | +```javascript | ||
| 319 | +test('新用户注册流程', async ({ page }) => { | ||
| 320 | + // 1. 访问注册页面 | ||
| 321 | + await page.goto('/register') | ||
| 322 | + | ||
| 323 | + // 2. 填写表单 | ||
| 324 | + await page.fill('input[name="email"]', 'test@example.com') | ||
| 325 | + await page.fill('input[name="password"]', 'password123') | ||
| 326 | + await page.fill('input[name="confirm"]', 'password123') | ||
| 327 | + | ||
| 328 | + // 3. 提交表单 | ||
| 329 | + await page.click('button[type="submit"]') | ||
| 330 | + | ||
| 331 | + // 4. 验证跳转 | ||
| 332 | + await expect(page).toHaveURL('/welcome') | ||
| 333 | + | ||
| 334 | + // 5. 验证提示信息 | ||
| 335 | + const toast = page.locator('.van-toast--success') | ||
| 336 | + await expect(toast).toBeVisible() | ||
| 337 | +}) | ||
| 338 | +``` | ||
| 339 | + | ||
| 340 | +### 2. 表单验证 | ||
| 341 | + | ||
| 342 | +```javascript | ||
| 343 | +test('登录表单验证', async ({ page }) => { | ||
| 344 | + await page.goto('/login') | ||
| 345 | + | ||
| 346 | + // 不输入任何信息,直接提交 | ||
| 347 | + await page.click('button[type="submit"]') | ||
| 348 | + | ||
| 349 | + // 验证错误提示 | ||
| 350 | + const errorToast = page.locator('.van-toast--fail') | ||
| 351 | + await expect(errorToast).toBeVisible() | ||
| 352 | + | ||
| 353 | + // 输入无效邮箱 | ||
| 354 | + await page.fill('input[name="email"]', 'invalid-email') | ||
| 355 | + await page.click('button[type="submit"]') | ||
| 356 | + | ||
| 357 | + // 验证错误提示 | ||
| 358 | + await expect(errorToast).toContainText('邮箱格式不正确') | ||
| 359 | +}) | ||
| 360 | +``` | ||
| 361 | + | ||
| 362 | +### 3. 响应式测试 | ||
| 363 | + | ||
| 364 | +```javascript | ||
| 365 | +test.describe('响应式布局', () => { | ||
| 366 | + test('移动端显示底部导航', async ({ page }) => { | ||
| 367 | + await page.setViewportSize({ width: 375, height: 667 }) | ||
| 368 | + await page.goto('/') | ||
| 369 | + | ||
| 370 | + const bottomNav = page.locator('.van-tabbar') | ||
| 371 | + await expect(bottomNav).toBeVisible() | ||
| 372 | + }) | ||
| 373 | + | ||
| 374 | + test('桌面端显示侧边栏', async ({ page }) => { | ||
| 375 | + await page.setViewportSize({ width: 1280, height: 720 }) | ||
| 376 | + await page.goto('/') | ||
| 377 | + | ||
| 378 | + const sidebar = page.locator('.sidebar') | ||
| 379 | + await expect(sidebar).toBeVisible() | ||
| 380 | + }) | ||
| 381 | +}) | ||
| 382 | +``` | ||
| 383 | + | ||
| 384 | +## 🔧 调试技巧 | ||
| 385 | + | ||
| 386 | +### 1. 使用 Playwright Inspector | ||
| 387 | + | ||
| 388 | +```bash | ||
| 389 | +# 启动 Inspector 模式 | ||
| 390 | +pnpm test:e2e:debug | ||
| 391 | + | ||
| 392 | +# 在代码中添加断点 | ||
| 393 | +await page.pause() // 暂停执行,打开 Inspector | ||
| 394 | +``` | ||
| 395 | + | ||
| 396 | +### 2. 查看执行过程 | ||
| 397 | + | ||
| 398 | +```bash | ||
| 399 | +# 有头模式(可以看到浏览器操作) | ||
| 400 | +pnpm test:e2e --headed | ||
| 401 | + | ||
| 402 | +# 慢动作模式 | ||
| 403 | +pnpm test:e2e --slow-mo=1000 # 每个操作延迟 1 秒 | ||
| 404 | +``` | ||
| 405 | + | ||
| 406 | +### 3. 详细日志 | ||
| 407 | + | ||
| 408 | +```javascript | ||
| 409 | +// 在测试中启用调试 | ||
| 410 | +test('调试示例', async ({ page }) => { | ||
| 411 | + // 启用详细日志 | ||
| 412 | + page.on('console', msg => console.log(msg.text())) | ||
| 413 | + page.on('pageerror', err => console.log(err)) | ||
| 414 | + | ||
| 415 | + await page.goto('/') | ||
| 416 | +}) | ||
| 417 | +``` | ||
| 418 | + | ||
| 419 | +## 📊 测试报告 | ||
| 420 | + | ||
| 421 | +### HTML 报告 | ||
| 422 | + | ||
| 423 | +```bash | ||
| 424 | +# 生成 HTML 报告 | ||
| 425 | +pnpm test:e2e | ||
| 426 | + | ||
| 427 | +# 查看报告 | ||
| 428 | +pnpm test:e2e:report | ||
| 429 | +``` | ||
| 430 | + | ||
| 431 | +**报告功能**: | ||
| 432 | + | ||
| 433 | +- 查看所有测试结果 | ||
| 434 | +- 查看截图和视频 | ||
| 435 | +- 查看时间线 | ||
| 436 | +- 追踪错误 | ||
| 437 | + | ||
| 438 | +### JSON 报告 | ||
| 439 | + | ||
| 440 | +```bash | ||
| 441 | +# JSON 报告会自动生成 | ||
| 442 | +cat test-results/test-results.json | ||
| 443 | +``` | ||
| 444 | + | ||
| 445 | +### JUnit 报告 | ||
| 446 | + | ||
| 447 | +```bash | ||
| 448 | +# JUnit 报告用于 CI 集成 | ||
| 449 | +cat test-results/test-results.xml | ||
| 450 | +``` | ||
| 451 | + | ||
| 452 | +## 🎭 配置选项 | ||
| 453 | + | ||
| 454 | +### 测试项目(projects) | ||
| 455 | + | ||
| 456 | +`playwright.config.js` 中配置了 3 个测试项目: | ||
| 457 | + | ||
| 458 | +```javascript | ||
| 459 | +projects: [ | ||
| 460 | + { | ||
| 461 | + name: 'chromium-mobile', | ||
| 462 | + use: { ...devices['iPhone 12'] }, | ||
| 463 | + }, | ||
| 464 | + { | ||
| 465 | + name: 'chromium-desktop', | ||
| 466 | + use: { ...devices['Desktop Chrome'] }, | ||
| 467 | + }, | ||
| 468 | + { | ||
| 469 | + name: 'webkit-mobile', | ||
| 470 | + use: { ...devices['iPhone 12'], browserName: 'webkit' }, | ||
| 471 | + }, | ||
| 472 | +] | ||
| 473 | +``` | ||
| 474 | + | ||
| 475 | +### 运行特定项目 | ||
| 476 | + | ||
| 477 | +```bash | ||
| 478 | +# 只运行移动端测试 | ||
| 479 | +pnpm test:e2e --project=chromium-mobile | ||
| 480 | + | ||
| 481 | +# 只运行桌面端测试 | ||
| 482 | +pnpm test:e2e --project=chromium-desktop | ||
| 483 | +``` | ||
| 484 | + | ||
| 485 | +### 失败重试 | ||
| 486 | + | ||
| 487 | +```javascript | ||
| 488 | +// CI 环境自动重试 2 次 | ||
| 489 | +retries: process.env.CI ? 2 : 0 | ||
| 490 | +``` | ||
| 491 | + | ||
| 492 | +### 超时配置 | ||
| 493 | + | ||
| 494 | +```javascript | ||
| 495 | +// 测试超时 30 秒 | ||
| 496 | +timeout: 30 * 1000 | ||
| 497 | + | ||
| 498 | +// 断言超时 5 秒 | ||
| 499 | +expect: { | ||
| 500 | + timeout: 5 * 1000 | ||
| 501 | +} | ||
| 502 | + | ||
| 503 | +// 操作超时 10 秒 | ||
| 504 | +use: { | ||
| 505 | + actionTimeout: 10 * 1000 | ||
| 506 | +} | ||
| 507 | +``` | ||
| 508 | + | ||
| 509 | +## 🚀 CI/CD 集成 | ||
| 510 | + | ||
| 511 | +### GitHub Actions | ||
| 512 | + | ||
| 513 | +```yaml | ||
| 514 | +name: E2E Tests | ||
| 515 | + | ||
| 516 | +on: [push, pull_request] | ||
| 517 | + | ||
| 518 | +jobs: | ||
| 519 | + test: | ||
| 520 | + runs-on: ubuntu-latest | ||
| 521 | + | ||
| 522 | + steps: | ||
| 523 | + - uses: actions/checkout@v3 | ||
| 524 | + | ||
| 525 | + - name: Setup Node.js | ||
| 526 | + uses: actions/setup-node@v3 | ||
| 527 | + with: | ||
| 528 | + node-version: 18 | ||
| 529 | + | ||
| 530 | + - name: Install pnpm | ||
| 531 | + uses: pnpm/action-setup@v2 | ||
| 532 | + | ||
| 533 | + - name: Install dependencies | ||
| 534 | + run: pnpm install | ||
| 535 | + | ||
| 536 | + - name: Install Playwright browsers | ||
| 537 | + run: npx playwright install --with-deps | ||
| 538 | + | ||
| 539 | + - name: Run E2E tests | ||
| 540 | + run: pnpm test:e2e | ||
| 541 | + | ||
| 542 | + - name: Upload test report | ||
| 543 | + if: always() | ||
| 544 | + uses: actions/upload-artifact@v3 | ||
| 545 | + with: | ||
| 546 | + name: playwright-report | ||
| 547 | + path: playwright-report/ | ||
| 548 | +``` | ||
| 549 | + | ||
| 550 | +## 📚 参考资源 | ||
| 551 | + | ||
| 552 | +- [Playwright 官方文档](https://playwright.dev) | ||
| 553 | +- [Playwright 中文文档](https://playwright.dev/docs/intro) | ||
| 554 | +- [最佳实践](https://playwright.dev/docs/best-practices) | ||
| 555 | +- [API 参考](https://playwright.dev/docs/api/class-playwright) | ||
| 556 | + | ||
| 557 | +## 🎉 总结 | ||
| 558 | + | ||
| 559 | +**Playwright 优势**: | ||
| 560 | + | ||
| 561 | +- ✅ 跨浏览器支持(Chromium、Firefox、WebKit) | ||
| 562 | +- ✅ 跨平台支持(Windows、macOS、Linux) | ||
| 563 | +- ✅ 快速可靠(并行执行、自动等待) | ||
| 564 | +- ✅ 强大的调试工具(Inspector、Trace、Video) | ||
| 565 | +- ✅ 完整的测试报告 | ||
| 566 | +- ✅ 移动端支持 | ||
| 567 | + | ||
| 568 | +**下一步**: | ||
| 569 | + | ||
| 570 | +- [ ] 编写核心功能的 E2E 测试 | ||
| 571 | +- [ ] 集成到 CI/CD 流程 | ||
| 572 | +- [ ] 定期更新测试用例 | ||
| 573 | + | ||
| 574 | +享受高质量的 E2E 测试!🚀 |
e2e/example.spec.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-01-28 21:45:00 | ||
| 3 | + * @Description: Playwright 示例 E2E 测试 | ||
| 4 | + */ | ||
| 5 | +import { test, expect } from '@playwright/test' | ||
| 6 | + | ||
| 7 | +test.describe('基础功能测试', () => { | ||
| 8 | + test('首页加载成功', async ({ page }) => { | ||
| 9 | + // 访问首页 | ||
| 10 | + await page.goto('/') | ||
| 11 | + | ||
| 12 | + // 等待页面加载 | ||
| 13 | + await page.waitForLoadState('networkidle') | ||
| 14 | + | ||
| 15 | + // 检查标题 | ||
| 16 | + await expect(page).toHaveTitle(/美乐爱觉/) | ||
| 17 | + | ||
| 18 | + // 检查关键元素 | ||
| 19 | + const header = page.locator('header').first() | ||
| 20 | + await expect(header).toBeVisible() | ||
| 21 | + }) | ||
| 22 | + | ||
| 23 | + test('导航功能正常', async ({ page }) => { | ||
| 24 | + await page.goto('/') | ||
| 25 | + | ||
| 26 | + // 点击课程列表 | ||
| 27 | + await page.click('text=课程') | ||
| 28 | + | ||
| 29 | + // 验证导航 | ||
| 30 | + await expect(page).toHaveURL(/\/courses-list/) | ||
| 31 | + }) | ||
| 32 | +}) | ||
| 33 | + | ||
| 34 | +test.describe('课程功能测试', () => { | ||
| 35 | + test('浏览课程列表', async ({ page }) => { | ||
| 36 | + await page.goto('/courses-list') | ||
| 37 | + | ||
| 38 | + // 等待课程列表加载 | ||
| 39 | + await page.waitForSelector('.course-card', { timeout: 5000 }) | ||
| 40 | + | ||
| 41 | + // 检查课程卡片是否存在 | ||
| 42 | + const courseCards = page.locator('.course-card') | ||
| 43 | + const count = await courseCards.count() | ||
| 44 | + | ||
| 45 | + expect(count).toBeGreaterThan(0) | ||
| 46 | + }) | ||
| 47 | + | ||
| 48 | + test('搜索课程', async ({ page }) => { | ||
| 49 | + await page.goto('/courses-list') | ||
| 50 | + | ||
| 51 | + // 输入搜索关键字 | ||
| 52 | + const searchInput = page.locator('input[placeholder*="搜索"]').first() | ||
| 53 | + await searchInput.fill('Vue') | ||
| 54 | + | ||
| 55 | + // 触发搜索 | ||
| 56 | + await searchInput.press('Enter') | ||
| 57 | + | ||
| 58 | + // 等待结果加载 | ||
| 59 | + await page.waitForTimeout(500) | ||
| 60 | + | ||
| 61 | + // 验证搜索结果 | ||
| 62 | + const courseCards = page.locator('.course-card') | ||
| 63 | + const count = await courseCards.count() | ||
| 64 | + | ||
| 65 | + expect(count).toBeGreaterThan(0) | ||
| 66 | + }) | ||
| 67 | +}) | ||
| 68 | + | ||
| 69 | +test.describe('用户认证测试', () => { | ||
| 70 | + test('显示登录页面', async ({ page }) => { | ||
| 71 | + await page.goto('/login') | ||
| 72 | + | ||
| 73 | + // 检查登录表单 | ||
| 74 | + const loginForm = page.locator('form').first() | ||
| 75 | + await expect(loginForm).toBeVisible() | ||
| 76 | + | ||
| 77 | + // 检查手机号输入框 | ||
| 78 | + const phoneInput = page.locator('input[name="phone"]') | ||
| 79 | + await expect(phoneInput).toBeVisible() | ||
| 80 | + }) | ||
| 81 | + | ||
| 82 | + test('登录表单验证', async ({ page }) => { | ||
| 83 | + await page.goto('/login') | ||
| 84 | + | ||
| 85 | + // 点击登录按钮(不输入信息) | ||
| 86 | + await page.click('button[type="submit"]') | ||
| 87 | + | ||
| 88 | + // 检查错误提示 | ||
| 89 | + const errorToast = page.locator('.van-toast--fail') | ||
| 90 | + await expect(errorToast).toBeVisible({ timeout: 3000 }) | ||
| 91 | + }) | ||
| 92 | +}) | ||
| 93 | + | ||
| 94 | +test.describe('响应式测试', () => { | ||
| 95 | + test('移动端布局正确', async ({ page }) => { | ||
| 96 | + // 设置移动端视口 | ||
| 97 | + await page.setViewportSize({ width: 375, height: 667 }) | ||
| 98 | + await page.goto('/') | ||
| 99 | + | ||
| 100 | + // 检查移动端导航 | ||
| 101 | + const bottomNav = page.locator('.van-tabbar') | ||
| 102 | + await expect(bottomNav).toBeVisible() | ||
| 103 | + }) | ||
| 104 | + | ||
| 105 | + test('桌面端布局正确', async ({ page }) => { | ||
| 106 | + // 设置桌面端视口 | ||
| 107 | + await page.setViewportSize({ width: 1280, height: 720 }) | ||
| 108 | + await page.goto('/') | ||
| 109 | + | ||
| 110 | + // 检查页面内容 | ||
| 111 | + const mainContent = page.locator('main').first() | ||
| 112 | + await expect(mainContent).toBeVisible() | ||
| 113 | + }) | ||
| 114 | +}) |
| ... | @@ -136,7 +136,7 @@ export default [ | ... | @@ -136,7 +136,7 @@ export default [ |
| 136 | }, | 136 | }, |
| 137 | }, | 137 | }, |
| 138 | 138 | ||
| 139 | - // 测试文件配置 | 139 | + // 测试文件配置(Vitest) |
| 140 | { | 140 | { |
| 141 | files: ['**/*.test.js', '**/*.spec.js', 'test/**'], | 141 | files: ['**/*.test.js', '**/*.spec.js', 'test/**'], |
| 142 | languageOptions: { | 142 | languageOptions: { |
| ... | @@ -154,6 +154,21 @@ export default [ | ... | @@ -154,6 +154,21 @@ export default [ |
| 154 | }, | 154 | }, |
| 155 | }, | 155 | }, |
| 156 | 156 | ||
| 157 | + // E2E 测试文件配置(Playwright) | ||
| 158 | + { | ||
| 159 | + files: ['e2e/**/*.{js,ts}'], | ||
| 160 | + languageOptions: { | ||
| 161 | + globals: { | ||
| 162 | + test: 'readonly', | ||
| 163 | + expect: 'readonly', | ||
| 164 | + beforeAll: 'readonly', | ||
| 165 | + afterAll: 'readonly', | ||
| 166 | + beforeEach: 'readonly', | ||
| 167 | + afterEach: 'readonly', | ||
| 168 | + }, | ||
| 169 | + }, | ||
| 170 | + }, | ||
| 171 | + | ||
| 157 | // Prettier 配置(必须最后) | 172 | // Prettier 配置(必须最后) |
| 158 | prettier, | 173 | prettier, |
| 159 | ] | 174 | ] | ... | ... |
| ... | @@ -10,6 +10,11 @@ | ... | @@ -10,6 +10,11 @@ |
| 10 | "test": "vitest run", | 10 | "test": "vitest run", |
| 11 | "test:ui": "vitest --ui", | 11 | "test:ui": "vitest --ui", |
| 12 | "test:coverage": "vitest --coverage", | 12 | "test:coverage": "vitest --coverage", |
| 13 | + "test:e2e": "playwright test", | ||
| 14 | + "test:e2e:ui": "playwright test --ui", | ||
| 15 | + "test:e2e:debug": "playwright test --debug", | ||
| 16 | + "test:e2e:headed": "playwright test --headed", | ||
| 17 | + "test:e2e:report": "playwright show-report", | ||
| 13 | "lint": "eslint . --fix", | 18 | "lint": "eslint . --fix", |
| 14 | "lint:check": "eslint .", | 19 | "lint:check": "eslint .", |
| 15 | "format": "prettier --write \"src/**/*.{js,vue,css,less,md,json}\"", | 20 | "format": "prettier --write \"src/**/*.{js,vue,css,less,md,json}\"", | ... | ... |
playwright.config.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-01-28 21:45:00 | ||
| 3 | + * @Description: Playwright E2E 测试配置 | ||
| 4 | + */ | ||
| 5 | +import { defineConfig, devices } from '@playwright/test' | ||
| 6 | + | ||
| 7 | +export default defineConfig({ | ||
| 8 | + // 测试文件位置 | ||
| 9 | + testDir: './e2e', | ||
| 10 | + | ||
| 11 | + // 测试超时时间(毫秒) | ||
| 12 | + timeout: 30 * 1000, | ||
| 13 | + | ||
| 14 | + // 期望超时时间 | ||
| 15 | + expect: { | ||
| 16 | + timeout: 5 * 1000, | ||
| 17 | + }, | ||
| 18 | + | ||
| 19 | + // 失败时重试次数 | ||
| 20 | + retries: process.env.CI ? 2 : 0, | ||
| 21 | + | ||
| 22 | + // 并行执行 | ||
| 23 | + workers: process.env.CI ? 1 : undefined, | ||
| 24 | + | ||
| 25 | + // 报告器 | ||
| 26 | + reporter: [ | ||
| 27 | + ['html', { outputFolder: 'playwright-report' }], | ||
| 28 | + ['json', { outputFile: 'test-results/test-results.json' }], | ||
| 29 | + ['junit', { outputFile: 'test-results/test-results.xml' }], | ||
| 30 | + ], | ||
| 31 | + | ||
| 32 | + // 共享配置 | ||
| 33 | + use: { | ||
| 34 | + // 基础 URL(开发服务器地址) | ||
| 35 | + baseURL: 'http://localhost:5173', | ||
| 36 | + | ||
| 37 | + // 追踪失败测试(用于调试) | ||
| 38 | + trace: 'on-first-retry', | ||
| 39 | + | ||
| 40 | + // 截图(仅失败时) | ||
| 41 | + screenshot: 'only-on-failure', | ||
| 42 | + | ||
| 43 | + // 视频录制(仅失败时) | ||
| 44 | + video: 'retain-on-failure', | ||
| 45 | + | ||
| 46 | + // 浏览器视口大小(移动端优先) | ||
| 47 | + viewport: { width: 375, height: 667 }, | ||
| 48 | + | ||
| 49 | + // 忽略 HTTPS 错误 | ||
| 50 | + ignoreHTTPSErrors: true, | ||
| 51 | + | ||
| 52 | + // 操作超时 | ||
| 53 | + actionTimeout: 10 * 1000, | ||
| 54 | + navigationTimeout: 30 * 1000, | ||
| 55 | + }, | ||
| 56 | + | ||
| 57 | + // 测试项目(不同浏览器和视口) | ||
| 58 | + projects: [ | ||
| 59 | + { | ||
| 60 | + name: 'chromium-mobile', | ||
| 61 | + use: { | ||
| 62 | + ...devices['iPhone 12'], | ||
| 63 | + browserName: 'chromium', | ||
| 64 | + }, | ||
| 65 | + }, | ||
| 66 | + { | ||
| 67 | + name: 'chromium-desktop', | ||
| 68 | + use: { | ||
| 69 | + ...devices['Desktop Chrome'], | ||
| 70 | + viewport: { width: 1280, height: 720 }, | ||
| 71 | + }, | ||
| 72 | + }, | ||
| 73 | + { | ||
| 74 | + name: 'webkit-mobile', | ||
| 75 | + use: { | ||
| 76 | + ...devices['iPhone 12'], | ||
| 77 | + browserName: 'webkit', | ||
| 78 | + }, | ||
| 79 | + }, | ||
| 80 | + ], | ||
| 81 | + | ||
| 82 | + // 开发服务器(测试前启动) | ||
| 83 | + webServer: { | ||
| 84 | + command: 'pnpm dev', | ||
| 85 | + url: 'http://localhost:5173', | ||
| 86 | + reuseExistingServer: !process.env.CI, | ||
| 87 | + timeout: 120 * 1000, | ||
| 88 | + }, | ||
| 89 | +}) |
-
Please register or login to post a comment