# Story 1.6: Select 工具函数单元测试

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## Story

作为测试开发者，
我想要 Select 工具函数有充分的单元测试，
以便确保函数的正确性和稳定性。

## Acceptance Criteria

**Given** Select 工具函数已实现（Story 1.3、1.4 完成）
**When** 创建 `tests/unit/radix-select.test.ts`
**Then** 测试覆盖率 ≥ 80%（NFR29）
**And** 测试用例包括：成功选择、选项不存在、超时、错误处理
**And** 使用 Vitest 运行测试
**And** 所有测试通过

## Tasks / Subtasks

- [x] 创建单元测试文件 `tests/unit/radix-select.test.ts` (AC: 1, 2)
  - [x] 配置 Vitest 环境和必要的 mock
  - [x] 导入 `selectRadixOption` 和 `selectRadixOptionAsync` 函数
- [x] 实现 `selectRadixOption` 单元测试 (AC: 3, 4)
  - [x] 测试成功选择静态选项（data-testid 策略）
  - [x] 测试成功选择静态选项（aria-label 策略）
  - [x] 测试成功选择静态选项（text 策略）
  - [x] 测试选项不存在时抛出 E2ETestError
  - [x] 测试触发器未找到时抛出 E2ETestError
  - [x] 测试错误消息包含完整上下文信息
- [x] 实现 `selectRadixOptionAsync` 单元测试 (AC: 3, 4)
  - [x] 测试成功选择异步选项（默认配置）
  - [x] 测试自定义超时配置
  - [x] 测试禁用网络空闲等待
  - [x] 测试选项加载超时抛出 E2ETestError
  - [x] 测试重试机制正常工作
- [x] 配置 Vitest 覆盖率目标 (AC: 1)
  - [x] 确认覆盖率配置 ≥ 80%
  - [x] 生成覆盖率报告
- [x] 验证测试通过 (AC: 5)
  - [x] 运行 `pnpm test:unit` 确保所有测试通过
  - [x] 检查覆盖率是否达标

## Dev Notes

### Epic 1 背景

**Epic 1 目标：** 测试开发者可以安装 `@d8d/e2e-test-utils` 包，立即使用 Select 工具测试 Radix UI Select 组件。

**本故事在 Epic 中的位置：** 第六个故事，为 Select 工具函数创建单元测试，确保代码质量和稳定性。

### 当前包状态分析

**已完成的工作（Story 1.1-1.5）：**

1. **Story 1.1** - 包基础结构（package.json、tsconfig.json、vitest.config.ts）
2. **Story 1.2** - 类型定义和错误处理（types.ts、errors.ts、constants.ts）
3. **Story 1.3** - 静态 Select 工具（`selectRadixOption()` 函数）
4. **Story 1.4** - 异步 Select 工具（`selectRadixOptionAsync()` 函数）
5. **Story 1.5** - 主导出和 README 文档

**本故事核心任务：** 为 Select 工具函数创建完整的单元测试，确保测试覆盖率 ≥ 80%。

### 单元测试架构约束

**从 Architecture 文档的测试策略：**

| 测试类型 | 范围 | 工具 | 目标 |
|----------|------|------|------|
| 单元测试 | 单个函数逻辑 | **Vitest** | ≥80% 覆盖率 |
| 集成测试 | 与 DOM/浏览器交互 | **Playwright** | 验证实际操作 |
| 稳定性测试 | 20次连续运行 | **Playwright** | 100% 通过率 |

**本故事专注于单元测试** - 测试纯函数逻辑，不需要真实的浏览器环境。

### Vitest 配置

**当前配置 (`vitest.config.ts`)：**
```typescript
{
  test: {
    dir: './tests/unit',
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      statements: 80,
      branches: 80,
      functions: 80,
      lines: 80
    },
    testTimeout: 10000
  }
}
```

**关键配置说明：**
- `environment: 'node'` - 单元测试在 Node.js 环境运行，不需要浏览器
- `coverage.statements: 80` - 语句覆盖率目标 ≥ 80%
- `testTimeout: 10000` - 每个测试超时 10 秒

### 单元测试设计策略

**测试函数的内部逻辑，而非 Playwright API：**

单元测试需要 mock Playwright 的 Page 对象，测试以下逻辑：

**`selectRadixOption` 需要测试的逻辑：**
1. 选择器策略优先级（data-testid → aria-label → text）
2. 触发器查找失败处理
3. 选项查找失败处理
4. E2ETestError 抛出和错误消息格式

**`selectRadixOptionAsync` 需要测试的逻辑：**
1. 默认配置合并（timeout、waitForOption、waitForNetworkIdle）
2. 网络空闲等待处理
3. 选项加载重试机制
4. 超时处理

### Mock 策略

**使用 Vitest 的 vi.mock() 来模拟 Playwright Page 对象：**

```typescript
import { vi, describe, it, expect } from 'vitest';
import { selectRadixOption } from '@d8d/e2e-test-utils';

// Mock Playwright Page 对象
const mockPage = {
  waitForSelector: vi.fn(),
  locator: vi.fn(),
  click: vi.fn(),
  waitForLoadState: vi.fn(),
  waitForTimeout: vi.fn()
} as any;
```

### 测试用例设计

**`selectRadixOption` 测试用例：**

| 测试场景 | 验证内容 | Mock 设置 |
|---------|---------|----------|
| 成功选择（data-testid） | 触发器和选项被正确点击 | waitForSelector 返回元素 |
| 成功选择（aria-label） | 第二策略生效 | 第一策略失败，第二成功 |
| 成功选择（text） | 兜底策略生效 | 前两策略失败，第三成功 |
| 触发器未找到 | 抛出 E2ETestError | 所有策略都失败 |
| 选项不存在 | 抛出 E2ETestError 并包含可用选项 | 触发器成功，选项失败 |

**`selectRadixOptionAsync` 测试用例：**

| 测试场景 | 验证内容 | Mock 设置 |
|---------|---------|----------|
| 默认配置成功 | 使用默认 timeout 和等待选项 | waitForLoadState 正常调用 |
| 自定义超时 | 使用传入的 timeout 值 | 传入 options 对象 |
| 禁用网络空闲等待 | 不调用 waitForLoadState | waitForNetworkIdle: false |
| 选项加载超时 | 抛出超时错误 | 选项选择器超时 |
| 重试机制 | 多次尝试查找选项 | 第一次失败，第二次成功 |

### 错误处理测试

**必须验证 E2ETestError 的完整上下文：**

```typescript
it('should throw E2ETestError with full context when trigger not found', async () => {
  // Mock 所有策略都失败
  mockPage.waitForSelector.mockRejectedValue(new Error('Not found'));

  await expect(selectRadixOption(mockPage, '残疾类型', '视力残疾'))
    .rejects.toThrow(E2ETestError);

  // 验证错误上下文
  try {
    await selectRadixOption(mockPage, '残疾类型', '视力残疾');
  } catch (error) {
    expect(error).toBeInstanceOf(E2ETestError);
    expect(error.context.operation).toBe('selectRadixOption');
    expect(error.context.target).toBe('残疾类型');
    expect(error.context.expected).toBe('视力残疾');
    expect(error.context.suggestion).toContain('data-testid');
  }
});
```

### 覆盖率目标

**根据 NFR29，测试覆盖率必须 ≥ 80%：**

**覆盖的代码路径：**
- `selectRadixOption`:
  - ✅ findTrigger 的三种选择器策略
  - ✅ findAndClickOption 的两种选项查找策略
  - ✅ 错误抛出路径
- `selectRadixOptionAsync`:
  - ✅ 配置合并逻辑
  - ✅ 网络空闲等待（可选）
  - ✅ waitForOptionAndSelect 的重试机制
  - ✅ 超时错误路径

**运行覆盖率报告：**
```bash
# 运行测试并生成覆盖率
pnpm test:coverage

# 或使用 Vitest 直接运行
pnpm vitest run --coverage
```

### 与前一个故事的集成

**Story 1.5 已完成的工作：**
- ✅ `src/index.ts` 导出 `selectRadixOption` 和 `selectRadixOptionAsync`
- ✅ 完整的 JSDoc 注释包含参数说明和示例
- ✅ README 文档包含 Select 工具使用示例

**本故事需要测试的代码：**
- `src/radix-select.ts` 中的所有导出函数
- `src/errors.ts` 中的 `E2ETestError` 和 `throwError`
- `src/constants.ts` 中的 `DEFAULT_TIMEOUTS` 常量使用

### 前一个故事的关键经验

**从 Story 1.3、1.4 中学习到的经验：**

1. **内部函数使用 `@internal` 标记**：
   - `findTrigger()` - 内部函数，不直接测试
   - `findAndClickOption()` - 内部函数，通过公共函数间接测试
   - `waitForOptionAndSelect()` - 内部函数，通过异步函数间接测试

2. **选择器策略优先级**：
   - 第一优先级：`data-testid` - 推荐，最稳定
   - 第二优先级：`aria-label` + `role` - 无障碍属性
   - 第三优先级：`text content` - 兜底方案

3. **错误处理模式**：
   - 使用 `throwError()` 辅助函数抛出 `E2ETestError`
   - 错误消息包含：operation、target、expected、available、suggestion

4. **超时配置**：
   - 静态选项：`DEFAULT_TIMEOUTS.static = 2000ms`
   - 异步选项：`DEFAULT_TIMEOUTS.async = 5000ms`
   - 网络空闲：`DEFAULT_TIMEOUTS.networkIdle = 10000ms`

### 文件结构

**测试文件位置：**
```
packages/e2e-test-utils/
├── src/
│   └── radix-select.ts      # 被测试的源文件
├── tests/
│   └── unit/
│       └── radix-select.test.ts  # 本故事创建的测试文件
├── vitest.config.ts          # Vitest 配置（已存在）
└── package.json
```

**导入路径：**
```typescript
// 测试文件中导入被测试函数
import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
import { E2ETestError } from '@d8d/e2e-test-utils';
import { DEFAULT_TIMEOUTS } from '@d8d/e2e-test-utils';
```

### 项目标准对齐

**与项目标准对齐：**
- 遵循 `docs/standards/testing-standards.md` 中的测试规范
- 使用 Vitest 作为单元测试运行器
- 使用 `console.debug` 进行调试输出（Vitest 中只有 `console.debug` 会显示）
- 测试文件命名：`radix-select.test.ts`（与源文件同名）

**Vitest 特殊规则：**
- **Console 输出**: 只有 `console.debug` 会显示，`console.log` 被屏蔽
- **Mock**: 使用 `vi.mocked()` 配合 `import` 语句，不用 `require`
- **运行测试**: `pnpm test --testNamePattern "测试名称"` 运行特定测试

### 前一个 Story 的智能信息

**从 Story 1.5 中获取的上下文：**

1. **README 文档位置**: `packages/e2e-test-utils/README.md`
2. **导出结构已确认正确**: `src/index.ts` 使用显式命名导出
3. **JSDoc 注释完整**: 所有公共 API 都有完整文档
4. **静态 vs 异步 Select 区分**: README 包含详细对比表格

**从 Story 1.4 中学到的经验：**
- 网络空闲等待失败不应中断流程（某些场景网络可能始终不空闲）
- 重试机制使用 100ms 间隔
- 超时后需要获取可用选项用于错误提示

### 性能约束

**从 NFR 提取的性能要求：**
- **NFR8**: 单个 Radix UI Select 选择操作（静态）应在 2 秒内完成
- **NFR9**: 单个 Radix UI Select 选择操作（异步）应在 5 秒内完成
- **NFR10**: 工具函数本身的开销不超过 100ms

**单元测试验证：**
- 测试执行时间应符合性能标准
- 覆盖率报告应包含执行时间统计

### 错误消息标准

**从 Architecture 文档的错误处理策略：**

```
❌ selectOption failed
Target: 残疾类型
Expected: 视力残疾
Available: 听力残疾, 言语残疾, 肢体残疾
💡 Suggestion: Check if the option value matches exactly
```

**测试必须验证：**
1. 错误类型是 `E2ETestError`
2. 错误包含完整的 `ErrorContext`
3. 错误消息格式符合标准

### Project Structure Notes

**对齐项目 Monorepo 架构：**
- 包位于 `packages/e2e-test-utils/`
- 使用 workspace 协议安装：`@d8d/e2e-test-utils@workspace:*`
- 测试文件位于 `tests/unit/` 目录

**与项目标准对齐：**
- 遵循 `docs/standards/testing-standards.md` 中的测试规范
- 遵循 `docs/standards/e2e-radix-testing.md` 中的 Radix UI E2E 测试标准

### References

**PRD 来源：**
- [PRD - E2E测试工具包](_bmad-output/planning-artifacts/prd.md) - 项目需求概述
- [PRD - 测试质量和稳定性保障](_bmad-output/planning-artifacts/epics.md#测试质量和稳定性保障-fr41-fr45) - FR41-FR45 需求

**Architecture 来源：**
- [Architecture - 测试策略](_bmad-output/planning-artifacts/architecture.md#testing-configuration) - 三层测试策略
- [Architecture - 实现模式](_bmad-output/planning-artifacts/architecture.md#implementation-patterns--consistency-rules) - 测试文件命名和结构模式

**标准文档来源：**
- [E2E Radix UI 测试标准](docs/standards/e2e-radix-testing.md) - 核心测试标准文档
- [Project Context](_bmad-output/project-context.md) - 项目技术栈和测试规则

**Epic 来源：**
- [Epic 1 - Story 1.6](_bmad-output/planning-artifacts/epics.md#story-16-select-工具函数单元测试) - 原始用户故事和验收标准

**前一个故事：**
- [Story 1.1 - 创建包基础结构和配置](_bmad-output/implementation-artifacts/1-1-create-package-structure.md) - 包基础设施和 vitest.config.ts
- [Story 1.2 - 实现类型定义和错误处理](_bmad-output/implementation-artifacts/1-2-implement-types-errors.md) - E2ETestError 和 ErrorContext
- [Story 1.3 - 实现静态 Select 工具函数](_bmad-output/implementation-artifacts/1-3-static-select-tool.md) - selectRadixOption 实现
- [Story 1.4 - 实现异步 Select 工具函数](_bmad-output/implementation-artifacts/1-4-async-select-tool.md) - selectRadixOptionAsync 实现
- [Story 1.5 - 创建主导出和基础文档](_bmad-output/implementation-artifacts/1-5-main-export-docs.md) - README 和导出结构

## Dev Agent Record

### Agent Model Used

Claude (d8d-model) via create-story workflow

### Debug Log References

### Completion Notes List

- 故事创建时间: 2026-01-09
- 基于 PRD、Architecture、E2E Radix 测试标准文档创建
- 基于 Story 1.1-1.5 的实现状态创建
- 包含完整的单元测试设计指南和 Mock 策略
- 包含测试用例设计表格和覆盖率目标

**实现完成时间: 2026-01-09**

**代码审查修复 (2026-01-09):**
- ✅ 添加了 index.ts 导出测试（8 个测试用例验证主导出配置）
- ✅ 添加了重试机制测试（2 个测试用例验证重试逻辑）
- ✅ 改进了错误处理测试深度（验证 suggestion 和 available 字段）
- ✅ 修复了 vitest.config.ts 覆盖率配置（排除 dist/、.d.ts、config 文件）
- ✅ 测试通过：37/37 (新增 10 个测试)
- ✅ 覆盖率达到要求：**Statements 93.65%, Branches 88.09%, Functions 100%**

**实际实现:**
- ✅ 创建了 `tests/unit/radix-select.test.ts` 单元测试文件
- ✅ 实现了 24 个测试用例（静态 + 异步 + 导出验证 + 重试机制）
- ✅ 所有测试通过 (37/37，包括 index.test.ts 中的 13 个测试)
- ✅ 覆盖率达到要求：Statements 93.65%, Branches 88.09%, Functions 100%
- ✅ 修复了源代码中的一个 bug（`selectRadixOptionAsync` 的网络空闲等待超时应该使用自定义 timeout）

**测试覆盖的功能:**
1. `selectRadixOption`:
   - data-testid 策略成功选择
   - aria-label 策略成功选择
   - text 策略成功选择
   - 触发器未找到错误处理
   - 选项不存在错误处理
   - 错误消息包含完整上下文

2. `selectRadixOptionAsync`:
   - 默认配置成功选择
   - 自定义超时配置
   - 禁用网络空闲等待
   - 选项加载超时错误处理
   - 超时错误包含重试建议
   - 触发器未找到错误处理
   - 选项查找策略优先级

**实现建议:**
- 使用 Vitest 的 `vi.fn()` 和 `vi.mocked()` 来 mock Playwright Page 对象
- 测试选择器策略的优先级顺序（data-testid → aria-label → text）
- 验证 E2ETestError 的完整上下文信息
- 确保覆盖率 ≥ 80%（NFR29）

**关键检查点:**
- ✅ 单元测试使用 Vitest 运行（非 Playwright）
- ✅ Mock Page 对象，不依赖真实浏览器
- ✅ 测试所有代码路径（成功、失败、错误）
- ✅ 验证错误消息格式和上下文完整性

### File List

**本故事创建的文件：**
- `packages/e2e-test-utils/tests/unit/radix-select.test.ts` - Select 工具函数的单元测试

**本故事修改的文件：**
- `packages/e2e-test-utils/src/radix-select.ts` - 修复了 `selectRadixOptionAsync` 中网络空闲等待使用自定义 timeout 的 bug
- `packages/e2e-test-utils/vitest.config.ts` - 修复覆盖率配置，排除 dist/、*.d.ts、vitest.config.ts 等非源代码文件

**相关文件（已在 Story 1.1-1.4 中完成，本故事测试）：**
- `packages/e2e-test-utils/src/types.ts` - 类型定义
- `packages/e2e-test-utils/src/errors.ts` - 错误类（被测试）
- `packages/e2e-test-utils/src/constants.ts` - 常量定义
- `packages/e2e-test-utils/vitest.config.ts` - Vitest 配置（已存在）

**只读参考文件：**
- `_bmad-output/implementation-artifacts/1-3-static-select-tool.md` - 静态 Select 实现
- `_bmad-output/implementation-artifacts/1-4-async-select-tool.md` - 异步 Select 实现
- `_bmad-output/planning-artifacts/epics.md` - Epic 和故事定义
- `_bmad-output/planning-artifacts/architecture.md` - 架构决策和测试策略
- `docs/standards/e2e-radix-testing.md` - E2E Radix UI 测试标准

