hookehuyr

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>
...@@ -44,3 +44,8 @@ mlaj ...@@ -44,3 +44,8 @@ mlaj
44 44
45 # Plan directory video resources 45 # Plan directory video resources
46 docs/plan/*/video/ 46 docs/plan/*/video/
47 +
48 +# Playwright E2E 测试
49 +test-results/
50 +playwright-report/
51 +playwright/.cache/
......
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 测试!🚀
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}\"",
......
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 +})