# Selector 组件测试计划文档

## 1. 测试概述

### 1.1 组件信息

| 项目 | 内容 |
|------|------|
| 组件名称 | Selector |
| 组件路径 | `packages/private-materials/src/pro/Selector` |
| 组件类型 | React 函数组件 (forwardRef) |
| 依赖 Hook | `useSelectionController` |
| 子组件 | `CardItem`、`NumericStepper` |

### 1.2 测试范围

- ✅ **核心功能**：选择、切换、数量控制、校验
- ✅ **UI 渲染**：不同变体、布局、样式
- ✅ **交互行为**：点击、切换、输入
- ✅ **边界条件**：空数据、极端值、异常输入
- ✅ **集成场景**：与 SelectorGroup 组合使用

### 1.3 测试策略

| 测试类型 | 工具 | 优先级 | 覆盖率目标 |
|---------|------|--------|-----------|
| 单元测试 | Vitest + RTL | P0 | 80%+ |
| 集成测试 | Vitest + RTL | P1 | 70%+ |
| 交互测试 | RTL + user-event | P1 | 60%+ |
| 快照测试 | Vitest | P2 | 核心组件 |
| 可视化测试 | Storybook | P1 | 核心场景 |
| E2E 测试 | Playwright (可选) | P2 | 关键流程 |

---

## 2. 测试环境配置

### 2.1 测试工具链

```typescript
// 测试框架
- Vitest: ^1.6.0
- @testing-library/react: ^12.1.5
- @testing-library/user-event: ^14.6.1
- @testing-library/jest-dom: ^5.16.5

// 测试环境
- jsdom: ^22.1.0 (浏览器环境模拟)
```

### 2.2 Mock 配置

参考 `src/test/setup.ts` 中的 Mock 配置：
- ✅ Ant Design 组件 Mock
- ✅ 国际化 locales Mock
- ✅ EngineContext Mock
- ✅ CSS Modules Mock

### 2.3 测试辅助函数

```typescript
// 测试工具函数示例
export const renderSelector = (props: Partial<SelectorProps>) => {
  return render(<Selector {...defaultProps} {...props} />);
};

export const createMockDataSource = (count: number) => {
  return Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    value: i + 1,
    label: `选项${i + 1}`,
    title: `选项${i + 1}`,
  }));
};
```

---

## 3. 测试数据准备

### 3.1 基础测试数据

```typescript
// 最小数据集
export const minimalDataSource = [
  { id: 1, value: 1, label: '选项1', title: '选项1' },
  { id: 2, value: 2, label: '选项2', title: '选项2' },
];

// 标准数据集
export const standardDataSource = [
  { id: 1, value: 1, label: '选项1', title: '选项1' },
  { id: 2, value: 2, label: '选项2', title: '选项2' },
  { id: 3, value: 3, label: '选项3', title: '选项3' },
  { id: 4, value: 4, label: '选项4', title: '选项4' },
  { id: 5, value: 5, label: '选项5', title: '选项5' },
];

// 带数量限制的数据集
export const quantityDataSource = [
  { 
    id: 1, 
    value: 1, 
    label: '选项1', 
    title: '选项1',
    ruleConfig: { min: 1, max: 10 }
  },
  { 
    id: 2, 
    value: 2, 
    label: '选项2', 
    title: '选项2',
    ruleConfig: { min: 0, max: 5 }
  },
];

// 带互斥规则的数据集
// 互斥规则说明：
// - 选项1、2、3 属于互斥组1，同一组内只能选择一个
// - 选项4、5、6 属于互斥组2，同一组内只能选择一个
// 使用时需要在 ruleConfig 中配置：mutex: [[1, 2, 3], [4, 5, 6]]
export const mutexDataSource = [
  { id: 1, value: 1, label: '选项1（互斥组1）', title: '选项1' },
  { id: 2, value: 2, label: '选项2（互斥组1）', title: '选项2' },
  { id: 3, value: 3, label: '选项3（互斥组1）', title: '选项3' },
  { id: 4, value: 4, label: '选项4（互斥组2）', title: '选项4' },
  { id: 5, value: 5, label: '选项5（互斥组2）', title: '选项5' },
  { id: 6, value: 6, label: '选项6（互斥组2）', title: '选项6' },
];

// 互斥规则配置示例（用于测试）
export const mutexRuleConfig = {
  mutex: [
    [1, 2, 3],  // 互斥组1：选项1、2、3 只能选一个
    [4, 5, 6],  // 互斥组2：选项4、5、6 只能选一个
  ],
};
```

### 3.2 边界测试数据

```typescript
// 空数据
export const emptyDataSource: OptionItem[] = [];

// 单条数据
export const singleItemDataSource = [
  { id: 1, value: 1, label: '唯一选项', title: '唯一选项' },
];

// 大量数据（性能测试）
export const largeDataSource = Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  value: i + 1,
  label: `选项${i + 1}`,
  title: `选项${i + 1}`,
}));

// 异常数据
export const invalidDataSource = [
  { id: null, value: undefined, label: '', title: '' }, // 缺失必填字段
  { id: 2, value: 2, label: '选项2', title: '选项2' },
];
```

### 3.3 默认 Props

```typescript
export const defaultSelectorProps: Partial<SelectorProps> = {
  id: 'test-selector',
  title: '测试标题',
  dataSource: standardDataSource,
  mode: 'single',
  valueType: 'primitive',
  fieldNames: { value: 'value', label: 'label' },
};
```

---

## 4. 功能测试用例

### 4.1 基础渲染测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-F-001 | 组件正常渲染 | 渲染组件，传入基础 props | 组件成功渲染，无报错 | P0 |
| TC-F-002 | 渲染选项列表 | 传入 dataSource，检查选项数量 | 所有选项正确渲染 | P0 |
| TC-F-003 | 空数据源渲染 | 传入空数组 dataSource | 显示空状态或空列表，无报错 | P1 |
| TC-F-004 | 自定义 fieldNames | 传入自定义 fieldNames | 选项使用自定义字段映射 | P1 |

**测试代码示例**：
```typescript
describe('基础渲染测试', () => {
  test('TC-F-001: 组件正常渲染', () => {
    const { container } = render(<Selector {...defaultProps} />);
    expect(container).toBeInTheDocument();
  });

  test('TC-F-002: 渲染选项列表', () => {
    const { getAllByRole } = render(
      <Selector dataSource={standardDataSource} />
    );
    const radios = getAllByRole('radio');
    expect(radios).toHaveLength(standardDataSource.length);
  });
});
```

### 4.2 单选模式测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-S-001 | 单选模式渲染 | mode='single'，检查使用 Radio | 使用 Radio 组件 | P0 |
| TC-S-002 | 选择选项 | 点击未选中选项 | 选项被选中，onChange 触发 | P0 |
| TC-S-003 | 取消选择 | 点击已选中选项 | 选项被取消，值变为 undefined | P0 |
| TC-S-004 | 切换选择 | 选择新选项 | 旧选项自动取消，新选项选中 | P0 |
| TC-S-005 | 原始值返回 | valueType='primitive' | onChange 返回原始值 | P0 |
| TC-S-006 | 对象值返回 | valueType='object' | onChange 返回对象值 | P0 |

**测试代码示例**：
```typescript
describe('单选模式测试', () => {
  test('TC-S-002: 选择选项', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();
    
    const { getAllByRole } = render(
      <Selector
        dataSource={standardDataSource}
        mode="single"
        valueType="primitive"
        onChange={handleChange}
      />
    );
    
    const radios = getAllByRole('radio');
    await user.click(radios[0]);
    
    expect(handleChange).toHaveBeenCalledWith(1);
    expect(radios[0]).toBeChecked();
  });
});
```

### 4.3 多选模式测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-M-001 | 多选模式渲染 | mode='multiple'，检查使用 Checkbox | 使用 Checkbox 组件 | P0 |
| TC-M-002 | 多选选项 | 依次选择多个选项 | 多个选项同时选中 | P0 |
| TC-M-003 | 取消单个选项 | 点击已选中选项 | 仅该选项取消，其他保持 | P0 |
| TC-M-004 | 原始值数组返回 | valueType='primitive' | onChange 返回数组 | P0 |
| TC-M-005 | 对象值数组返回 | valueType='object' | onChange 返回对象数组 | P0 |

### 4.4 数量选择测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-Q-001 | 数量选择器显示 | 选项 max > 1，valueType='object' | 显示数量选择器 | P0 |
| TC-Q-002 | 数量修改 | 修改数量值 | onChange 触发，值包含 quantity | P0 |
| TC-Q-003 | 数量为 0 自动取消 | 将数量调为 0 | 选项自动取消选择 | P0 |
| TC-Q-004 | 数量限制 min | 数量小于 min | 显示错误提示 | P1 |
| TC-Q-005 | 数量限制 max | 数量大于 max | 显示错误提示 | P1 |
| TC-Q-006 | 选项级数量限制 | 不同选项不同限制 | 各自限制生效 | P1 |

### 4.5 校验规则测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-V-001 | required 必选校验 | required=1，未选择 | validate 失败，显示错误 | P0 |
| TC-V-002 | min 最小数量校验 | min=2，选择少于 2 个 | validate 失败 | P0 |
| TC-V-003 | max 最大数量校验 | max=3，选择超过 3 个 | validate 失败 | P0 |
| TC-V-004 | mutex 互斥组校验 | 选择同组多个选项 | 自动取消同组其他选项 | P0 |
| TC-V-005 | customValidator 自定义校验 | 传入自定义校验器 | 校验器逻辑生效 | P1 |
| TC-V-006 | 自动校验 | autoValidate=true | 选择时自动校验 | P1 |

**测试代码示例**：
```typescript
describe('校验规则测试', () => {
  test('TC-V-001: required 必选校验', async () => {
    const ref = React.createRef<any>();
    
    render(
      <Selector
        ref={ref}
        dataSource={standardDataSource}
        ruleConfig={{ required: 1 }}
      />
    );
    
    await expect(ref.current.validate()).rejects.toBeDefined();
    const errors = ref.current.getErrors();
    expect(errors.some(e => e.type === 'required')).toBe(true);
  });
});
```

### 4.6 Ref 方法测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-R-001 | validate 方法 | 调用 ref.validate() | 返回 Promise，执行校验 | P0 |
| TC-R-002 | getErrors 方法 | 调用 ref.getErrors() | 返回错误数组 | P0 |
| TC-R-003 | reset 方法 | 调用 ref.reset() | 重置为 defaultValue | P0 |
| TC-R-004 | clear 方法 | 调用 ref.clear() | 清空所有选择 | P0 |

---

## 5. UI/交互测试用例

### 5.1 布局测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-U-001 | 默认布局 | 不传 grid | 垂直列表布局 | P1 |
| TC-U-002 | 网格布局 2 列 | grid={{ columns: 2 }} | 选项呈 2 列排列 | P1 |
| TC-U-003 | 网格布局 3 列 | grid={{ columns: 3 }} | 选项呈 3 列排列 | P1 |
| TC-U-004 | 网格间距 | grid={{ gutter: 24 }} | 选项间距正确 | P2 |
| TC-U-005 | 网格对齐 | grid={{ align: 'middle' }} | 对齐方式生效 | P2 |

### 5.2 样式变体测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-SV-001 | default 变体 | variant='default' | 默认 Radio/Checkbox | P1 |
| TC-SV-002 | card 变体 | variant='card' | 渲染卡片样式 | P1 |
| TC-SV-003 | select 变体 | variant='select' | 渲染 Select 下拉 | P1 |
| TC-SV-004 | 卡片布局 vertical | item={{ layout: 'vertical' }} | 垂直布局 | P1 |
| TC-SV-005 | 卡片布局 horizontal | item={{ layout: 'horizontal' }} | 水平布局 | P1 |
| TC-SV-006 | 选中态样式 filled | item={{ selectedTypeVariant: 'filled' }} | 填充样式 | P1 |
| TC-SV-007 | 选中态样式 cornered | item={{ selectedTypeVariant: 'cornered' }} | 角标样式 | P1 |

### 5.3 标题与提示测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-T-001 | 标题显示 | 传入 title | 标题正确显示 | P1 |
| TC-T-002 | 标题隐藏 | titleProps={{ visible: false }} | 标题隐藏 | P1 |
| TC-T-003 | 自定义标题 | titleProps={{ title: { text: '自定义' } }} | 显示自定义文本 | P1 |
| TC-T-004 | 提示信息 | titleProps={{ tip: { text: '提示' } }} | 提示正确显示 | P1 |
| TC-T-005 | 自动提示 | ruleConfig 有 min/max | 自动生成提示文本 | P1 |
| TC-T-006 | 必选标签 | ruleConfig={{ required: 1 }} | 显示必选标签 | P1 |
| TC-T-007 | 错误信息显示 | 校验失败 | 错误信息显示 | P0 |

### 5.4 交互测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-I-001 | 点击选项选择 | 点击未选中选项 | 选项选中，状态更新 | P0 |
| TC-I-002 | 点击已选项取消 | 点击已选中选项 | 选项取消，状态更新 | P0 |
| TC-I-003 | 禁用选项不可点击 | 点击 disabled 选项 | 无响应，状态不变 | P1 |
| TC-I-004 | 组件禁用 | disabled=true | 所有选项禁用 | P1 |
| TC-I-005 | 数量步进器交互 | 点击 +/- 按钮 | 数量正确增减 | P0 |
| TC-I-006 | 快速连续操作 | 快速点击多个选项 | 状态正确，无异常 | P1 |

**测试代码示例**：
```typescript
describe('交互测试', () => {
  test('TC-I-001: 点击选项选择', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();
    
    const { getAllByRole } = render(
      <Selector
        dataSource={standardDataSource}
        mode="single"
        onChange={handleChange}
      />
    );
    
    const radios = getAllByRole('radio');
    await user.click(radios[0]);
    
    expect(handleChange).toHaveBeenCalled();
    expect(radios[0]).toBeChecked();
  });
});
```

---

## 6. 边界与异常测试用例

### 6.1 数据边界测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-B-001 | 空数据源 | dataSource=[] | 正常渲染，不报错 | P1 |
| TC-B-002 | 单条数据 | dataSource 只有 1 条 | 正常渲染 | P1 |
| TC-B-003 | 大量数据 | dataSource 1000+ 条 | 性能可接受，正常渲染 | P2 |
| TC-B-004 | 重复 value | 多个选项相同 value | 处理正确或提示错误 | P1 |
| TC-B-005 | null/undefined 值 | value 为 null/undefined | 处理正确，不报错 | P1 |

### 6.2 值边界测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-B-006 | 数量为 0 | quantity=0 | 选项取消或保持 | P1 |
| TC-B-007 | 数量为负数 | quantity=-1 | 处理正确，不报错 | P1 |
| TC-B-008 | 数量超大值 | quantity=999999 | 受 max 限制或处理正确 | P1 |
| TC-B-009 | 无效 value | value 不在 dataSource | 处理正确，不报错 | P1 |

### 6.3 异常场景测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-E-001 | onChange 抛出异常 | onChange 中 throw Error | 不影响组件状态 | P2 |
| TC-E-002 | 校验器抛出异常 | customValidator throw | 错误正确处理 | P1 |
| TC-E-003 | 数据源动态变化 | dataSource 从有到无 | 状态正确处理 | P1 |
| TC-E-004 | 值动态变化 | value 外部变更 | 状态同步更新 | P0 |
| TC-E-005 | 规则动态变化 | ruleConfig 变更 | 重新校验生效 | P1 |

---

## 7. 性能测试用例

### 7.1 渲染性能测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-P-001 | 100 选项渲染 | 渲染 100 个选项 | 渲染时间 < 500ms | P2 |
| TC-P-002 | 1000 选项渲染 | 渲染 1000 个选项 | 渲染时间 < 2000ms | P2 |
| TC-P-003 | 大数据集交互 | 1000 选项中选择 | 交互响应 < 100ms | P2 |

### 7.2 内存测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-P-004 | 内存泄漏检测 | 创建销毁 100 次 | 无内存泄漏 | P2 |
| TC-P-005 | 大数据集内存 | 1000 选项内存占用 | 内存占用 < 50MB | P2 |

---

## 8. 可访问性测试用例

### 8.1 键盘导航测试

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-A-001 | Tab 键导航 | 按 Tab 切换焦点 | 焦点正确切换 | P1 |
| TC-A-002 | Space/Enter 选择 | 焦点在选项时按 Space | 选项选中/取消 | P1 |
| TC-A-003 | 方向键导航 | 使用方向键切换 | 选项正确切换 | P1 |

### 8.2 ARIA 与语义化测试

> **说明**：屏幕阅读器测试主要用于验证组件对视觉障碍用户的可访问性。在单元测试中，我们主要通过检查 ARIA 属性和语义化 HTML 来确保可访问性。

| 用例ID | 测试场景 | 测试步骤 | 预期结果 | 优先级 |
|--------|---------|---------|---------|--------|
| TC-A-004 | ARIA 标签检查 | 检查 Radio/Checkbox 的 aria-label | 有正确的 ARIA 标签 | P1 |
| TC-A-005 | 选中状态语义 | 检查 checked 状态 | aria-checked 正确反映状态 | P1 |
| TC-A-006 | 禁用状态语义 | 检查 disabled 属性 | aria-disabled 正确 | P1 |
| TC-A-007 | 错误信息语义 | 校验失败时检查 | 错误信息有适当的 ARIA 属性 | P2 |
| TC-A-008 | 表单关联 | 检查 label 关联 | label 与 input 正确关联 | P1 |

**测试代码示例**：
```typescript
describe('ARIA 可访问性测试', () => {
  test('TC-A-004: ARIA 标签检查', () => {
    const { getAllByRole } = render(
      <Selector
        dataSource={standardDataSource}
        mode="single"
        title="选择选项"
      />
    );
    
    const radios = getAllByRole('radio');
    // Radio 组件应该自动处理 ARIA 属性
    radios.forEach((radio, index) => {
      expect(radio).toHaveAttribute('aria-label', standardDataSource[index].label);
    });
  });

  test('TC-A-005: 选中状态语义', async () => {
    const user = userEvent.setup();
    const { getAllByRole } = render(
      <Selector
        dataSource={standardDataSource}
        mode="single"
      />
    );
    
    const radios = getAllByRole('radio');
    await user.click(radios[0]);
    
    expect(radios[0]).toHaveAttribute('aria-checked', 'true');
    expect(radios[1]).toHaveAttribute('aria-checked', 'false');
  });
});
```

**注意**：
- ✅ **单元测试中的可访问性测试**：主要检查 ARIA 属性和语义化 HTML，这是自动化的、可执行的
- ⚠️ **真实屏幕阅读器测试**：需要使用真实屏幕阅读器软件（如 NVDA、JAWS、VoiceOver）进行人工测试，属于 E2E 测试范畴
- 💡 **实际建议**：对于内部组件库，单元测试中的 ARIA 检查已足够；对于面向公众的产品，建议进行真实屏幕阅读器测试

---

## 9. 测试用例执行清单

### 9.1 测试用例分类统计

| 类别 | 用例数量 | P0 | P1 | P2 | 完成状态 |
|------|---------|-----|-----|-----|---------|
| 基础渲染 | 4 | 2 | 2 | 0 | ⬜ |
| 单选模式 | 6 | 6 | 0 | 0 | ⬜ |
| 多选模式 | 5 | 5 | 0 | 0 | ⬜ |
| 数量选择 | 6 | 2 | 4 | 0 | ⬜ |
| 校验规则 | 6 | 3 | 3 | 0 | ⬜ |
| Ref 方法 | 4 | 4 | 0 | 0 | ⬜ |
| UI 布局 | 5 | 0 | 3 | 2 | ⬜ |
| 样式变体 | 7 | 0 | 7 | 0 | ⬜ |
| 标题提示 | 7 | 1 | 6 | 0 | ⬜ |
| 交互测试 | 6 | 2 | 4 | 0 | ⬜ |
| 边界测试 | 9 | 0 | 6 | 3 | ⬜ |
| 异常测试 | 5 | 1 | 4 | 0 | ⬜ |
| 性能测试 | 5 | 0 | 0 | 5 | ⬜ |
| 可访问性 | 8 | 0 | 8 | 0 | ⬜ |
| Storybook 测试 | 16 | 5 | 10 | 1 | ⬜ |
| **总计** | **93** | **31** | **51** | **11** | **0%** |

### 9.2 测试执行优先级

#### Phase 1: 核心功能 (P0) - 31 个用例
- [ ] 基础渲染测试（2）
- [ ] 单选模式测试（6）
- [ ] 多选模式测试（5）
- [ ] 数量选择基础（2）
- [ ] 校验规则核心（3）
- [ ] Ref 方法测试（4）
- [ ] 标题错误显示（1）
- [ ] 交互核心（2）
- [ ] 值同步更新（1）
- [ ] Storybook 核心测试（5）

#### Phase 2: 重要功能 (P1) - 48 个用例
- [ ] 基础渲染扩展（2）
- [ ] 数量选择扩展（4）
- [ ] 校验规则扩展（3）
- [ ] UI 布局测试（3）
- [ ] 样式变体测试（7）
- [ ] 标题提示测试（6）
- [ ] 交互扩展（4）
- [ ] 边界测试（6）
- [ ] 异常测试（4）
- [ ] 可访问性测试（5）
- [ ] Storybook 测试（10）

#### Phase 3: 优化与完善 (P2) - 11 个用例
- [ ] UI 布局细节（2）
- [ ] 性能测试（5）
- [ ] 边界测试扩展（3）
- [ ] Storybook 自定义功能（1）

---

## 10. 测试覆盖率目标

### 10.1 覆盖率指标

| 指标 | 目标值 | 当前值 | 状态 |
|------|--------|--------|------|
| Statements | 80% | - | ⬜ |
| Branches | 75% | - | ⬜ |
| Functions | 80% | - | ⬜ |
| Lines | 80% | - | ⬜ |

---

## 11. 测试用例编写规范

### 11.1 命名规范

```typescript
// 测试文件命名
Selector.test.tsx
SelectorGroup.test.tsx
useSelectionController.test.ts

// 测试套件命名
describe('Selector 组件', () => {
  describe('单选模式', () => {
    // 测试用例
  });
});

// 测试用例命名
test('应该正确渲染组件', () => {});
test('应该在单选模式下使用 Radio', () => {});
```

### 11.2 测试用例结构

```typescript
describe('功能描述', () => {
  // Arrange: 准备测试数据和环境
  const defaultProps = { ... };
  
  // 清理函数（可选）
  afterEach(() => {
    cleanup();
  });

  test('具体测试场景', () => {
    // Arrange: 准备
    const handleChange = vi.fn();
    
    // Act: 执行
    const { ... } = render(<Selector {...props} />);
    // 执行操作...
    
    // Assert: 断言
    expect(...).toBe(...);
  });
});
```

### 11.3 断言最佳实践

```typescript
// ✅ 好的实践
expect(element).toBeInTheDocument();
expect(element).toHaveTextContent('文本');
expect(handleChange).toHaveBeenCalledWith(expectedValue);
expect(handleChange).toHaveBeenCalledTimes(1);

// ❌ 避免的实践
expect(element).not.toBeNull(); // 使用 toBeInTheDocument()
expect(getByText('文本')).toBeTruthy(); // 直接使用查询结果
```

### 11.4 异步测试处理

```typescript
// 使用 waitFor
test('异步更新测试', async () => {
  const { getByText } = render(<Selector />);
  
  await waitFor(() => {
    expect(getByText('更新后的文本')).toBeInTheDocument();
  });
});

// 使用 findBy
test('异步查询测试', async () => {
  const { findByText } = render(<Selector />);
  const element = await findByText('异步文本');
  expect(element).toBeInTheDocument();
});
```

---

## 12. Storybook 可视化测试

### 12.1 Storybook 概述

**Storybook** 是一个用于独立开发和测试 UI 组件的工具。它提供了一个隔离的环境来开发和测试组件，支持交互式组件预览、Props 调试、不同状态展示等。

**在组件测试中的作用**：
- ✅ **可视化验证**：直观查看组件在不同配置下的渲染效果
- ✅ **交互测试**：手动测试组件的交互行为
- ✅ **文档展示**：作为组件使用文档和示例
- ✅ **回归测试**：通过视觉回归测试发现 UI 变化
- ✅ **跨团队协作**：设计师和产品可以查看组件效果

### 12.2 Storybook 环境配置

#### 12.2.1 安装与启动

```bash
# 安装依赖（已在 package.json 中配置）
pnpm install

# 启动 Storybook
cd packages/private-materials
pnpm storybook

# Storybook 将在 http://localhost:6006 启动
```

#### 12.2.2 配置文件

**主要配置文件**：
- `.storybook/main.js` - Storybook 主配置
- `.storybook/preview.js` - Storybook 预览配置
- `src/pro/Selector/Selector.stories.tsx` - Selector 组件的 Stories

#### 12.2.3 依赖包

```json
{
  "devDependencies": {
    "@storybook/addon-essentials": "^7.6.19",
    "@storybook/addon-interactions": "^7.6.19",
    "@storybook/addon-links": "^7.6.19",
    "@storybook/blocks": "^7.6.19",
    "@storybook/react": "^7.6.19",
    "@storybook/react-webpack5": "^7.6.19",
    "storybook": "^7.6.19"
  }
}
```

### 12.3 Stories 测试用例

#### 12.3.1 Stories 清单

| Story 名称 | 测试场景 | 对应测试用例 | 优先级 |
|-----------|---------|-------------|--------|
| Default | 基础单选模式 | TC-S-001, TC-S-002 | P0 |
| Multiple | 多选模式 | TC-M-001, TC-M-002 | P0 |
| SingleWithQuantity | 单选 + 数量 | TC-Q-001, TC-Q-002 | P0 |
| MultipleWithQuantity | 多选 + 数量 | TC-Q-001, TC-M-002 | P0 |
| GridLayout2Columns | 2列网格布局 | TC-U-002 | P1 |
| GridLayout3Columns | 3列网格布局 | TC-U-003 | P1 |
| WithValidation | 校验规则 | TC-V-001 ~ TC-V-005 | P0 |
| MutexGroups | 互斥组功能 | TC-V-004 | P0 |
| Disabled | 禁用状态 | TC-I-003, TC-I-004 | P1 |
| SelectVariant | Select 变体 | TC-SV-003 | P1 |
| SelectVariantMultiple | 多选下拉 | TC-SV-003, TC-M-001 | P1 |
| CardVariants | 卡片样式变体 | TC-SV-002, TC-SV-006, TC-SV-007 | P1 |
| CustomIcon | 自定义图标 | - | P2 |
| CustomRender | 自定义渲染 | - | P2 |
| SelectorGroupBasic | 基础分组 | TC-G-001 | P1 |
| SelectorGroupWithLinkage | 联动规则 | TC-G-002 | P1 |

#### 12.3.2 Stories 使用示例

**基础 Story 定义**：

```typescript
import type { Meta, StoryObj } from '@storybook/react';
import Selector from './index';

const meta: Meta<typeof Selector> = {
  title: 'Pro Components/Selector',
  component: Selector,
  tags: ['autodocs'],
  parameters: {
    docs: {
      description: {
        component: '高级选择器组件，支持单选/多选、数量控制、校验规则等功能。',
      },
    },
  },
  argTypes: {
    mode: {
      control: 'select',
      options: ['single', 'multiple'],
      description: '选择模式：单选或多选',
    },
    valueType: {
      control: 'select',
      options: ['primitive', 'object'],
      description: '值类型：原始值或对象值',
    },
  },
};

export default meta;
type Story = StoryObj<typeof Selector>;

// 基础 Story 示例
export const Default: Story = {
  args: {
    id: 'default-selector',
    title: '基础单选',
    dataSource: [
      { id: 1, value: 1, label: '选项1', title: '选项1' },
      { id: 2, value: 2, label: '选项2', title: '选项2' },
    ],
    mode: 'single',
    valueType: 'primitive',
    fieldNames: { value: 'value', label: 'label' },
  },
  render: (args) => {
    const [value, setValue] = useState<SelectionValue>(1);
    return (
      <Selector
        {...args}
        value={value}
        onChange={(v) => {
          setValue(v);
          console.log('选中值:', v);
        }}
      />
    );
  },
};
```

### 12.4 Storybook 测试方法

#### 12.4.1 可视化测试

**测试步骤**：
1. 启动 Storybook：`pnpm storybook`
2. 在浏览器中打开 `http://localhost:6006`
3. 选择对应的 Story
4. 验证组件的视觉呈现是否符合预期

**测试检查项**：
- ✅ 组件正确渲染，无错误提示
- ✅ 样式正确显示（布局、颜色、间距等）
- ✅ 交互状态正确（悬停、选中、禁用等）
- ✅ 响应式布局正确（不同屏幕尺寸）

#### 12.4.2 交互测试

**使用 Storybook 的交互面板**：

1. **手动交互测试**：
   - 点击选项，验证选中状态
   - 调整数量，验证数量控制
   - 切换不同模式，验证行为差异

2. **Controls 面板测试**：
   - 调整 `mode` 属性，观察组件变化
   - 修改 `valueType`，验证值类型切换
   - 切换 `variant`，查看不同样式变体

3. **Actions 面板**：
   - 观察 `onChange` 回调的输出
   - 验证事件触发的参数是否正确

#### 12.4.3 边界场景测试

在 Storybook 中可以方便地测试各种边界场景：

```typescript
// 空数据源测试
export const EmptyDataSource: Story = {
  args: {
    dataSource: [],
    title: '空数据源',
  },
};

// 单条数据测试
export const SingleItem: Story = {
  args: {
    dataSource: [
      { id: 1, value: 1, label: '唯一选项', title: '唯一选项' },
    ],
  },
};

// 大量数据测试（性能验证）
export const LargeDataset: Story = {
  args: {
    dataSource: Array.from({ length: 100 }, (_, i) => ({
      id: i + 1,
      value: i + 1,
      label: `选项${i + 1}`,
      title: `选项${i + 1}`,
    })),
  },
};
```

#### 12.4.4 跨浏览器测试

使用 Storybook 在不同浏览器中测试：
- Chrome
- Firefox
- Safari
- Edge

**测试重点**：
- ✅ 样式兼容性
- ✅ 交互行为一致性
- ✅ 性能表现

### 12.5 Storybook 与单元测试的区别

| 对比项 | Storybook | 单元测试 (Vitest) |
|-------|-----------|-------------------|
| **测试类型** | 可视化、手动验证 | 自动化、断言验证 |
| **执行方式** | 手动运行，人工检查 | 自动运行，CI/CD |
| **测试场景** | UI 渲染、交互体验 | 逻辑正确性、边界条件 |
| **覆盖范围** | 核心场景、典型用例 | 全面覆盖、边界异常 |
| **反馈速度** | 即时可视化 | 快速执行结果 |
| **维护成本** | 需要人工检查 | 自动化维护 |
| **适用场景** | 开发调试、UI 验收 | 回归测试、CI 集成 |

**互补关系**：
- ✅ **Storybook**：用于开发阶段的快速验证和 UI 验收
- ✅ **单元测试**：用于保证逻辑正确性和自动化回归测试
- ✅ **两者结合**：Storybook 确保 UI 正确，单元测试确保逻辑正确

### 12.6 Storybook 测试用例执行清单

#### 12.6.1 基础功能验证

- [ ] **Default**: 单选模式正常渲染
- [ ] **Multiple**: 多选模式正常渲染
- [ ] **SingleWithQuantity**: 单选 + 数量功能正常
- [ ] **MultipleWithQuantity**: 多选 + 数量功能正常

#### 12.6.2 布局验证

- [ ] **GridLayout2Columns**: 2列布局正确
- [ ] **GridLayout3Columns**: 3列布局正确
- [ ] 响应式布局在不同屏幕尺寸下正常

#### 12.6.3 校验功能验证

- [ ] **WithValidation**: 校验规则生效
- [ ] **MutexGroups**: 互斥组功能正确
- [ ] 错误信息正确显示

#### 12.6.4 样式变体验证

- [ ] **SelectVariant**: Select 变体正常
- [ ] **CardVariants**: 卡片样式变体正确
- [ ] **CustomIcon**: 自定义图标正常显示
- [ ] **CustomRender**: 自定义渲染正确

#### 12.6.5 分组组件验证

- [ ] **SelectorGroupBasic**: 基础分组功能
- [ ] **SelectorGroupWithLinkage**: 联动规则生效

### 12.7 Storybook 测试最佳实践

#### 12.7.1 Story 组织原则

```typescript
// ✅ 好的实践：按功能分组
export default {
  title: 'Pro Components/Selector',
  component: Selector,
};

// ✅ 好的实践：清晰的 Story 命名
export const SingleSelection = { ... };
export const MultipleSelection = { ... };
export const WithQuantity = { ... };

// ❌ 避免的实践：模糊的命名
export const Test1 = { ... };
export const Example = { ... };
```

#### 12.7.2 Story 参数配置

```typescript
// ✅ 好的实践：提供完整的 argTypes
const meta: Meta<typeof Selector> = {
  argTypes: {
    mode: {
      control: 'select',
      options: ['single', 'multiple'],
      description: '选择模式',
      table: {
        type: { summary: 'single | multiple' },
        defaultValue: { summary: 'single' },
      },
    },
  },
};

// ✅ 好的实践：使用真实数据
export const RealWorldExample: Story = {
  args: {
    dataSource: realisticDataSource, // 使用真实业务数据
  },
};
```

#### 12.7.3 交互式测试

```typescript
// ✅ 好的实践：使用 Controls 进行交互测试
export const InteractiveExample: Story = {
  args: {
    // 所有 props 都可以通过 Controls 面板调整
  },
  parameters: {
    docs: {
      description: {
        story: '使用 Controls 面板调整属性，实时查看组件变化。',
      },
    },
  },
};
```
---

## 13. 测试维护指南

### 13.1 测试用例更新

当组件发生变更时：
1. ✅ 检查现有测试用例是否需要更新
2. ✅ 添加新功能的测试用例
3. ✅ 更新测试数据以覆盖新场景
4. ✅ 确保所有测试通过

### 13.2 测试用例审查清单

- [ ] 测试用例覆盖核心功能
- [ ] 边界条件有测试覆盖
- [ ] 异常情况有处理
- [ ] 测试代码可读性强
- [ ] 测试数据合理
- [ ] 断言准确明确
- [ ] 无冗余测试

---

## 14. 附录

### 14.1 测试工具参考

- [Vitest 文档](https://vitest.dev/)
- [React Testing Library 文档](https://testing-library.com/react)
- [Testing Library 查询优先级](https://testing-library.com/docs/queries/about/#priority)
- [Storybook 文档](https://storybook.js.org/)
- [Storybook React 文档](https://storybook.js.org/docs/react/get-started/introduction)

### 14.2 相关文档

- [开发文档](https://studio.picoding-dev.com/pro-components/selector?tab=dev)
- [设计文档](https://m1ed09stz4r.feishu.cn/wiki/NL5AwOs0ei5ZBekT4i6cuzIOnoM)
- [Storybook 使用指南](./STORYBOOK.md)
