docs: 添加设计文档和实现计划
- 添加菜单数据修复设计文档 - 添加用户管理和角色管理测试修复设计文档 - 添加本地开发测试设计文档 - 添加相关实现计划
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# E2E 测试选择器指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了 NovaVis 睿视项目中实际使用的选择器,用于 E2E 测试。
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. **优先使用文本选择器** - 更稳定,不易受 UI 库变更影响
|
||||
2. **避免依赖 data-testid** - 除非确实存在
|
||||
3. **使用页面快照验证** - 确保选择器正确
|
||||
|
||||
## 页面选择器映射
|
||||
|
||||
### 案件管理页面
|
||||
|
||||
| 元素 | 选择器 | 说明 |
|
||||
|------|--------|------|
|
||||
| 案件列表项 | `button:has-text("进入")` | 使用"进入"按钮定位案件 |
|
||||
| 第一个案件 | `button:has-text("进入")` | 选择第一个案件 |
|
||||
| 案件卡片 | `generic[cursor=pointer]` | 卡片容器 |
|
||||
|
||||
### 导航菜单
|
||||
|
||||
| 元素 | 选择器 | 说明 |
|
||||
|------|--------|------|
|
||||
| 案件管理 | `menuitem:has-text("案件管理")` | 主菜单项 |
|
||||
| 概览 | `menuitem:has-text("概览")` | 子菜单项 |
|
||||
| 数据管理 | `menuitem:has-text("数据管理")` | 主菜单项 |
|
||||
| 关系分析 | `menuitem:has-text("关系分析")` | 主菜单项 |
|
||||
| 资金流向 | `menuitem:has-text("资金流向")` | 主菜单项 |
|
||||
| AI 分析 | `menuitem:has-text("AI 分析")` | 主菜单项 |
|
||||
| 报告中心 | `menuitem:has-text("报告中心")` | 主菜单项 |
|
||||
| 标注管理 | `menuitem:has-text("标注管理")` | 主菜单项 |
|
||||
| 系统设置 | `menuitem:has-text("系统设置")` | 主菜单项 |
|
||||
|
||||
### 数据导入页面
|
||||
|
||||
| 元素 | 选择器 | 说明 |
|
||||
|------|--------|------|
|
||||
| 页面标题 | `h3:has-text("数据导入")` | 页面标题 |
|
||||
| 导入步骤 | `[data-testid="import-steps"]` | 步骤指示器 |
|
||||
| 文件输入 | `input[type="file"]` | 文件上传输入框 |
|
||||
|
||||
### 网络图谱页面
|
||||
|
||||
| 元素 | 选择器 | 说明 |
|
||||
|------|--------|------|
|
||||
| 页面标题 | `h2:has-text("资金流向与关系网络分析")` | 页面标题 |
|
||||
| 数据源选择器 | `[data-testid="network-graph-datasource-selector"]` | 数据源下拉框 |
|
||||
| 布局选择器 | `[data-testid="network-graph-layout-selector"]` | 布局下拉框 |
|
||||
| 节点搜索 | `input[placeholder*="搜索"]` | 搜索输入框 |
|
||||
|
||||
### 报告生成页面
|
||||
|
||||
| 元素 | 选择器 | 说明 |
|
||||
|------|--------|------|
|
||||
| 页面标题 | `h2:has-text("报告生成")` | 页面标题 |
|
||||
| 模板选择器 | `.ant-select` | 模板下拉框 |
|
||||
| 生成按钮 | `button:has-text("生成报告")` | 生成按钮 |
|
||||
|
||||
## 交互处理
|
||||
|
||||
### 按钮被遮挡
|
||||
|
||||
**问题**:侧边栏遮挡了按钮,导致点击失败
|
||||
|
||||
**解决方案**:
|
||||
```typescript
|
||||
const button = page.locator('button:has-text("进入")').first()
|
||||
await button.scrollIntoViewIfNeeded()
|
||||
await button.click({ force: true })
|
||||
```
|
||||
|
||||
### 页面默认状态
|
||||
|
||||
**问题**:页面默认显示案件管理页面,不需要导航
|
||||
|
||||
**解决方案**:
|
||||
```typescript
|
||||
// ❌ 错误:尝试导航到案件管理
|
||||
await page.click('[data-testid="nav-cases"]')
|
||||
|
||||
// ✅ 正确:页面默认显示案件管理,直接操作
|
||||
const enterButton = page.locator('button:has-text("进入")').first()
|
||||
await enterButton.click({ force: true })
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用页面快照验证选择器**
|
||||
```typescript
|
||||
const snapshot = await page.locator('body').innerHTML()
|
||||
console.log(snapshot)
|
||||
```
|
||||
|
||||
2. **优先使用文本选择器**
|
||||
```typescript
|
||||
// ✅ 推荐
|
||||
page.locator('button:has-text("进入")')
|
||||
|
||||
// ❌ 不推荐(除非确实存在)
|
||||
page.locator('[data-testid="enter-button"]')
|
||||
```
|
||||
|
||||
3. **处理异步加载**
|
||||
```typescript
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.waitForTimeout(500)
|
||||
```
|
||||
|
||||
4. **错误处理**
|
||||
```typescript
|
||||
try {
|
||||
await element.click({ timeout: 5000 })
|
||||
} catch (error) {
|
||||
console.log('Element not found or not clickable')
|
||||
}
|
||||
```
|
||||
@@ -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 友好
|
||||
Reference in New Issue
Block a user