docs: 添加设计文档和实现计划

- 添加菜单数据修复设计文档
- 添加用户管理和角色管理测试修复设计文档
- 添加本地开发测试设计文档
- 添加相关实现计划
This commit is contained in:
张翔
2026-04-15 23:36:27 +08:00
parent 5c402e49da
commit 38dc055a27
9 changed files with 6959 additions and 0 deletions
@@ -0,0 +1,385 @@
# E2E 测试最佳实践
## 概述
本文档记录了 NovaVis 睿视项目的 E2E 测试最佳实践。
## 核心原则
### 1. 测试应该验证真实用户行为
**问题**:测试只验证元素存在,不验证实际功能
**解决方案**
```typescript
// ❌ 错误:只验证元素可见
const button = page.locator('button')
await expect(button).toBeVisible()
// ✅ 正确:验证实际功能
const button = page.locator('button:has-text("进入")').first()
await button.scrollIntoViewIfNeeded()
await button.click({ force: true })
// 验证页面跳转
await page.waitForURL('**/overview')
const title = await page.locator('h1').textContent()
expect(title).toContain('概览')
```
### 2. 使用硬验证而非软验证
**问题**:测试使用软验证,即使页面没有内容也能通过
**解决方案**
```typescript
// ❌ 错误:软验证
const dataStats = page.locator('[data-testid="data-stats"]')
if (await dataStats.isVisible()) {
const statsText = await dataStats.textContent()
expect(statsText).toBeTruthy()
}
// ✅ 正确:硬验证
const dataStats = page.locator('[data-testid="data-stats"]')
await dataStats.waitFor({ state: 'visible', timeout: 5000 })
const statsText = await dataStats.textContent()
expect(statsText).toBeTruthy()
expect(statsText!.length).toBeGreaterThan(10)
```
### 3. 验证数据流而非仅 UI
**问题**:测试只验证 UI,不验证数据是否正确加载
**解决方案**
```typescript
// ❌ 错误:只验证 UI
const table = page.locator('.ant-table')
await expect(table).toBeVisible()
// ✅ 正确:验证数据流
const table = page.locator('.ant-table')
await expect(table).toBeVisible()
// 验证表格有数据
const rows = await table.locator('.ant-table-row').count()
expect(rows).toBeGreaterThan(0)
// 验证数据内容
const firstRowText = await table.locator('.ant-table-row').first().textContent()
expect(firstRowText).toBeTruthy()
expect(firstRowText!.length).toBeGreaterThan(5)
```
## 测试结构
### 1. 使用 test.step 组织测试步骤
```typescript
test('应该能够导入数据', async ({ page }) => {
await test.step('1. 导航到数据导入页面', async () => {
await page.goto('/data-import')
await page.waitForLoadState('networkidle')
})
await test.step('2. 选择文件', async () => {
const fileInput = page.locator('input[type="file"]')
await fileInput.setInputFiles('test-data/sample.xlsx')
})
await test.step('3. 验证导入成功', async () => {
const successMessage = page.locator('.ant-message-success')
await expect(successMessage).toBeVisible()
})
})
```
### 2. 使用 Page Object Model
```typescript
// pages/DataImportPage.ts
export class DataImportPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('/data-import')
await this.page.waitForLoadState('networkidle')
}
async selectFile(filePath: string) {
const fileInput = this.page.locator('input[type="file"]')
await fileInput.setInputFiles(filePath)
}
async verifyImportSuccess() {
const successMessage = this.page.locator('.ant-message-success')
await expect(successMessage).toBeVisible()
}
}
```
### 3. 使用 Workflow 封装业务流程
```typescript
// workflows/DataImportWorkflow.ts
export class DataImportWorkflow {
constructor(private page: Page) {}
async importFile(filePath: string) {
await this.navigateToImport()
await this.selectFile(filePath)
await this.verifyImportSuccess()
}
private async navigateToImport() {
// 导航逻辑
}
private async selectFile(filePath: string) {
// 选择文件逻辑
}
private async verifyImportSuccess() {
// 验证逻辑
}
}
```
## 选择器策略
### 1. 优先级
1. **文本选择器** - 最稳定
```typescript
page.locator('button:has-text("提交")')
```
2. **角色选择器** - 语义化
```typescript
page.getByRole('button', { name: '提交' })
```
3. **标签选择器** - 简单
```typescript
page.locator('h1')
```
4. **data-testid** - 最后选择
```typescript
page.locator('[data-testid="submit-button"]')
```
### 2. 避免使用的选择器
❌ **CSS 类名** - 容易变化
```typescript
page.locator('.ant-btn-primary')
```
❌ **复杂的 CSS 选择器** - 脆弱
```typescript
page.locator('div > div > button.ant-btn.ant-btn-primary')
```
## 等待策略
### 1. 使用自动等待
```typescript
// ✅ 推荐:Playwright 自动等待
await page.click('button')
// ❌ 不推荐:手动等待
await page.waitForTimeout(1000)
await page.click('button')
```
### 2. 明确等待条件
```typescript
// ✅ 推荐:明确等待条件
await page.waitForSelector('.ant-table-row', { state: 'visible' })
// ❌ 不推荐:模糊等待
await page.waitForTimeout(2000)
```
### 3. 等待网络请求
```typescript
// ✅ 推荐:等待网络请求完成
await page.waitForLoadState('networkidle')
// 或者等待特定请求
await page.waitForResponse(response =>
response.url().includes('/api/data') && response.status() === 200
)
```
## 错误处理
### 1. 使用 try-catch 处理可选操作
```typescript
try {
const optionalButton = page.locator('button:has-text("可选操作")')
await optionalButton.click({ timeout: 5000 })
} catch (error) {
console.log('可选操作按钮不存在,跳过')
}
```
### 2. 使用条件判断
```typescript
const cancelButton = page.locator('button:has-text("取消")')
if (await cancelButton.isVisible()) {
await cancelButton.click()
}
```
## 调试技巧
### 1. 使用页面快照
```typescript
const snapshot = await page.locator('body').innerHTML()
console.log('Page snapshot:', snapshot)
```
### 2. 使用截图
```typescript
await page.screenshot({ path: 'debug.png', fullPage: true })
```
### 3. 使用 trace
```typescript
// 在 playwright.config.ts 中启用
use: {
trace: 'on-first-retry',
}
```
## 测试数据
### 1. 使用测试数据工厂
```typescript
// fixtures/test-data-factory.ts
export class TestDataFactory {
async createExcel(rows: number): Promise<string> {
// 创建测试 Excel 文件
}
async createLargeExcel(rows: number): Promise<string> {
// 创建大型测试 Excel 文件
}
async createCorruptedExcel(): Promise<string> {
// 创建损坏的 Excel 文件
}
}
```
### 2. 使用 fixtures
```typescript
// fixtures/test-fixtures.ts
import { test as base } from '@playwright/test'
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// 登录逻辑
await page.goto('/login')
await page.fill('input[name="username"]', 'testuser')
await page.fill('input[name="password"]', 'password')
await page.click('button[type="submit"]')
await use(page)
},
})
```
## 性能优化
### 1. 并行执行
```typescript
// playwright.config.ts
export default defineConfig({
workers: 4, // 并行执行 4 个测试
})
```
### 2. 重用登录状态
```typescript
// 使用 storageState 重用登录状态
export default defineConfig({
use: {
storageState: 'auth.json',
},
})
```
### 3. 跳过不必要的等待
```typescript
// ❌ 不推荐:全局等待
await page.waitForTimeout(2000)
// ✅ 推荐:精确等待
await page.waitForSelector('.ant-table-row')
```
## CI/CD 集成
### 1. 使用 Docker
```dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx playwright install --with-deps
```
### 2. 使用 GitHub Actions
```yaml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
```
## 总结
遵循这些最佳实践,可以确保 E2E 测试:
1. ✅ 验证真实用户行为
2. ✅ 使用硬验证
3. ✅ 验证数据流
4. ✅ 使用稳定的选择器
5. ✅ 正确处理等待
6. ✅ 良好的错误处理
7. ✅ 易于调试
8. ✅ 性能优化
9. ✅ CI/CD 友好