# Story 1.6: Select 工具函数单元测试 Status: done ## 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 测试标准