# 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(<UserForm />);

  // 填写表单
  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(<Button onClick={mockOnClick}>提交</Button>);

  fireEvent.click(screen.getByText('提交'));
  expect(mockOnClick).toHaveBeenCalled();
});
```

### 2. 使用数据测试ID
```typescript
// 组件代码
<Button data-testid="submit-button" type="submit">提交</Button>

// 测试代码
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(<UserList />);

  // 初始加载状态
  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: () => <div data-testid="mock-user-table" />
}));
```

### 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(<UserManagementPage />);

  // 打开创建表单
  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: () => <button data-testid="mock-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应用
