# Web UI 包测试规范
## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 1.0 | 2025-12-26 | 从测试策略文档拆分,专注Web UI包测试 | James (Claude Code) |
## 概述
本文档定义了Web UI包的测试标准和最佳实践,包括:
- **packages/*-ui**: Web UI组件包(用户管理UI、文件管理UI等)
- **web/tests/**: Web应用测试(组件测试、集成测试、E2E测试)
## 测试框架栈
### 单元测试和集成测试
- **Vitest**: 测试运行器
- **Testing Library**: React组件测试(`@testing-library/react`、`@testing-library/user-event`)
- **Happy DOM**: 轻量级DOM环境(`@happy-dom/global-registrator`)
### E2E测试
- **Playwright**: 端到端测试框架
## 测试分层策略
### 1. 组件测试(Component Tests)
- **范围**: 单个UI组件
- **目标**: 验证组件渲染、交互和状态
- **位置**: `packages/*-ui/tests/**/*.test.tsx` 或 `web/tests/unit/client/**/*.test.tsx`
- **框架**: Vitest + Testing Library
- **覆盖率目标**: ≥ 80%
### 2. 集成测试(Integration Tests)
- **范围**: 多个组件协作、与API集成
- **目标**: 验证组件间交互和数据流
- **位置**: `web/tests/integration/**/*.test.tsx`
- **框架**: Vitest + Testing Library + MSW(API模拟)
- **覆盖率目标**: ≥ 60%
### 3. E2E测试(End-to-End Tests)
- **范围**: 完整用户流程
- **目标**: 验证端到端业务流程
- **位置**: `web/tests/e2e/**/*.test.ts`
- **框架**: Playwright
- **覆盖率目标**: 关键用户流程100%
## 测试文件结构
### UI包结构
```
packages/user-management-ui/
├── src/
│ ├── components/
│ │ ├── UserTable.tsx
│ │ └── UserForm.tsx
│ └── index.ts
└── tests/
├── unit/
│ ├── UserTable.test.tsx
│ └── UserForm.test.tsx
└── integration/
└── UserManagementFlow.test.tsx
```
### Web应用结构
```
web/tests/
├── unit/
│ └── client/
│ ├── pages/
│ │ └── Users.test.tsx
│ └── components/
│ └── DataTablePagination.test.tsx
├── integration/
│ └── client/
│ └── user-management.test.tsx
└── e2e/
├── login.spec.ts
└── user-management.spec.ts
```
## 组件测试最佳实践
### 1. 使用Testing Library原则
```typescript
// ✅ 推荐:从用户角度测试
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('应该允许用户创建新用户', async () => {
const user = userEvent.setup();
render();
// 填写表单
await user.type(screen.getByLabelText(/用户名/), 'testuser');
await user.type(screen.getByLabelText(/邮箱/), 'test@example.com');
// 提交表单
await user.click(screen.getByRole('button', { name: /创建/ }));
// 验证结果
expect(screen.getByText(/创建成功/)).toBeInTheDocument();
});
// ❌ 避免:测试实现细节
test('submitButton onClick 应该被调用', () => {
const mockOnClick = vi.fn();
render();
fireEvent.click(screen.getByText('提交'));
expect(mockOnClick).toHaveBeenCalled();
});
```
### 2. 使用数据测试ID
```typescript
// 组件代码
// 测试代码
const submitButton = screen.getByTestId('submit-button');
await user.click(submitButton);
```
### 3. 模拟API调用
```typescript
import { vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { UserForm } from './UserForm';
// 模拟RPC客户端
vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
rpcClient: vi.fn(() => ({
users: {
create: {
$post: vi.fn(() => Promise.resolve({
status: 201,
json: async () => ({ id: 1, username: 'testuser' })
}))
}
}
}))
}));
```
### 4. 测试异步状态
```typescript
test('应该显示加载状态', async () => {
render();
// 初始加载状态
expect(screen.getByText(/加载中/)).toBeInTheDocument();
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText(/用户列表/)).toBeInTheDocument();
});
});
```
## 集成测试最佳实践
### 1. 使用真实组件而非模拟
```typescript
// ✅ 推荐:使用真实组件,模拟API
vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
rpcClient: vi.fn(() => ({
users: {
$get: vi.fn(() => Promise.resolve({
status: 200,
json: async () => ({ data: mockUsers })
}))
}
}))
}));
// ❌ 避免:模拟整个组件
vi.mock('@d8d/user-management-ui', () => ({
UserTable: () =>
}));
```
### 2. 提供完整的模拟数据
```typescript
const mockUsers = [
{
id: 1,
username: 'testuser',
email: 'test@example.com',
role: 'user',
createdAt: '2025-01-01T00:00:00.000Z',
// 包含真实组件需要的所有字段
}
];
```
### 3. 验证完整的用户流程
```typescript
test('应该成功创建用户并刷新列表', async () => {
const user = userEvent.setup();
render();
// 打开创建表单
await user.click(screen.getByTestId('create-user-button'));
// 填写表单
await user.type(screen.getByLabelText(/用户名/), 'newuser');
await user.type(screen.getByLabelText(/邮箱/), 'new@example.com'));
// 提交表单
await user.click(screen.getByRole('button', { name: /创建/ }));
// 验证成功消息
await waitFor(() => {
expect(screen.getByText(/创建成功/)).toBeInTheDocument();
});
// 验证列表刷新
await waitFor(() => {
expect(screen.getByText('newuser')).toBeInTheDocument();
});
});
```
### 4. 使用共享测试工具处理复杂组件
```typescript
import { completeRadixSelectFlow } from '@d8d/shared-ui-components/tests/utils';
// 处理Radix UI Select组件的完整选择流程
await completeRadixSelectFlow('role-selector', 'admin', { useFireEvent: true });
```
## E2E测试最佳实践
### 1. 使用Page Object模式
```typescript
// tests/e2e/pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.page.fill('input[name="username"]', username);
await this.page.fill('input[name="password"]', password);
await this.page.click('button[type="submit"]');
}
async expectWelcomeMessage() {
await expect(this.page.getByText(/欢迎/)).toBeVisible();
}
}
// 测试文件
test('用户登录流程', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password');
await loginPage.expectWelcomeMessage();
});
```
### 2. 使用稳定的定位器
```typescript
// ✅ 推荐:使用语义化定位器
await page.click('button:has-text("提交")');
await page.fill('input[placeholder="请输入用户名"]', 'testuser');
await page.click('[data-testid="submit-button"]');
// ❌ 避免:使用不稳定的CSS选择器
await page.click('.btn-primary');
```
### 3. 等待策略
```typescript
// 等待元素可见
await page.waitForSelector('[data-testid="user-list"]');
// 等待网络请求完成
await page.waitForLoadState('networkidle');
// 等待特定条件
await page.waitForURL('/dashboard');
```
## 测试命名约定
### 文件命名
- 组件测试:`[ComponentName].test.tsx`
- 集成测试:`[feature].integration.test.tsx`
- E2E测试:`[feature].spec.ts`
### 测试描述
```typescript
describe('UserForm', () => {
describe('表单验证', () => {
it('应该验证必填字段', async () => { });
it('应该验证邮箱格式', async () => { });
});
describe('表单提交', () => {
it('应该成功创建用户', async () => { });
it('应该处理网络错误', async () => { });
});
});
```
## 常见错误避免
### ❌ 不要测试实现细节
```typescript
// 错误:测试useState调用
expect(useState).toHaveBeenCalledWith([]);
// 正确:测试渲染结果
expect(screen.getByText('用户列表')).toBeInTheDocument();
```
### ❌ 不要过度模拟
```typescript
// 错误:模拟整个组件库
vi.mock('@d8d/shared-ui-components', () => ({
Button: () =>
}));
// 正确:使用真实组件,模拟其依赖
```
### ❌ 不要忽略异步操作
```typescript
// 错误:不等待异步操作
fireEvent.click(submitButton);
expect(successMessage).toBeInTheDocument();
// 正确:等待异步操作完成
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/成功/)).toBeInTheDocument();
});
```
## 覆盖率标准
| 测试类型 | 最低要求 | 目标要求 |
|----------|----------|----------|
| 组件测试 | 70% | 80% |
| 集成测试 | 50% | 60% |
| E2E测试 | 关键流程100% | 主要流程80% |
## 运行测试
### 本地开发
```bash
# 运行所有测试
pnpm test
# 运行组件测试
pnpm test:components
# 运行集成测试
pnpm test:integration
# 运行E2E测试
pnpm test:e2e:chromium
# 生成覆盖率报告
pnpm test:coverage
# 运行特定测试
pnpm test --testNamePattern="UserForm"
```
### CI/CD
```yaml
web-component-tests:
runs-on: ubuntu-latest
steps:
- run: cd web && pnpm test:components
web-integration-tests:
runs-on: ubuntu-latest
steps:
- run: cd web && pnpm test:integration
web-e2e-tests:
runs-on: ubuntu-latest
steps:
- run: cd web && pnpm test:e2e:chromium
```
## 调试技巧
### 1. 使用调试模式
```bash
# 运行特定测试并显示详细信息
pnpm test --testNamePattern="UserForm" --reporter=verbose
# 在浏览器中打开调试器
pnpm test:components --debug
```
### 2. 查看DOM结构
```typescript
// 在测试中打印DOM结构
screen.debug();
// 打印特定元素
screen.debug(screen.getByTestId('user-table'));
```
### 3. E2E测试调试
```typescript
// 使用调试模式
test('调试模式', async ({ page }) => {
await page.goto('/users');
await page.pause(); // 暂停执行,打开Playwright Inspector
});
```
## 参考实现
- 用户管理UI包:`packages/user-management-ui`
- 文件管理UI包:`packages/file-management-ui`
- Web应用测试:`web/tests/`
## 相关文档
- [测试策略概述](./testing-strategy.md)
- [后端模块包测试规范](./backend-module-testing-standards.md)
- [Web Server包测试规范](./web-server-testing-standards.md)
- [Mini UI包测试规范](./mini-ui-testing-standards.md)
- [UI包开发规范](./ui-package-standards.md)
---
**文档状态**: 正式版
**适用范围**: Web UI包和Web应用