# 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: () =>