Status: done
作为测试开发者, 我想要 Select 工具函数有充分的单元测试, 以便确保函数的正确性和稳定性。
Given Select 工具函数已实现(Story 1.3、1.4 完成)
When 创建 tests/unit/radix-select.test.ts
Then 测试覆盖率 ≥ 80%(NFR29)
And 测试用例包括:成功选择、选项不存在、超时、错误处理
And 使用 Vitest 运行测试
And 所有测试通过
tests/unit/radix-select.test.ts (AC: 1, 2)
selectRadixOption 和 selectRadixOptionAsync 函数selectRadixOption 单元测试 (AC: 3, 4)
selectRadixOptionAsync 单元测试 (AC: 3, 4)
pnpm test:unit 确保所有测试通过Epic 1 目标: 测试开发者可以安装 @d8d/e2e-test-utils 包,立即使用 Select 工具测试 Radix UI Select 组件。
本故事在 Epic 中的位置: 第六个故事,为 Select 工具函数创建单元测试,确保代码质量和稳定性。
已完成的工作(Story 1.1-1.5):
selectRadixOption() 函数)selectRadixOptionAsync() 函数)本故事核心任务: 为 Select 工具函数创建完整的单元测试,确保测试覆盖率 ≥ 80%。
从 Architecture 文档的测试策略:
| 测试类型 | 范围 | 工具 | 目标 |
|---|---|---|---|
| 单元测试 | 单个函数逻辑 | Vitest | ≥80% 覆盖率 |
| 集成测试 | 与 DOM/浏览器交互 | Playwright | 验证实际操作 |
| 稳定性测试 | 20次连续运行 | Playwright | 100% 通过率 |
本故事专注于单元测试 - 测试纯函数逻辑,不需要真实的浏览器环境。
当前配置 (vitest.config.ts):
{
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 需要测试的逻辑:
selectRadixOptionAsync 需要测试的逻辑:
使用 Vitest 的 vi.mock() 来模拟 Playwright Page 对象:
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 的完整上下文:
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:
selectRadixOptionAsync:
运行覆盖率报告:
# 运行测试并生成覆盖率
pnpm test:coverage
# 或使用 Vitest 直接运行
pnpm vitest run --coverage
Story 1.5 已完成的工作:
src/index.ts 导出 selectRadixOption 和 selectRadixOptionAsync本故事需要测试的代码:
src/radix-select.ts 中的所有导出函数src/errors.ts 中的 E2ETestError 和 throwErrorsrc/constants.ts 中的 DEFAULT_TIMEOUTS 常量使用从 Story 1.3、1.4 中学习到的经验:
内部函数使用 @internal 标记:
findTrigger() - 内部函数,不直接测试findAndClickOption() - 内部函数,通过公共函数间接测试waitForOptionAndSelect() - 内部函数,通过异步函数间接测试选择器策略优先级:
data-testid - 推荐,最稳定aria-label + role - 无障碍属性text content - 兜底方案错误处理模式:
throwError() 辅助函数抛出 E2ETestError超时配置:
DEFAULT_TIMEOUTS.static = 2000msDEFAULT_TIMEOUTS.async = 5000msDEFAULT_TIMEOUTS.networkIdle = 10000ms测试文件位置:
packages/e2e-test-utils/
├── src/
│ └── radix-select.ts # 被测试的源文件
├── tests/
│ └── unit/
│ └── radix-select.test.ts # 本故事创建的测试文件
├── vitest.config.ts # Vitest 配置(已存在)
└── package.json
导入路径:
// 测试文件中导入被测试函数
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 中的测试规范console.debug 进行调试输出(Vitest 中只有 console.debug 会显示)radix-select.test.ts(与源文件同名)Vitest 特殊规则:
console.debug 会显示,console.log 被屏蔽vi.mocked() 配合 import 语句,不用 requirepnpm test --testNamePattern "测试名称" 运行特定测试从 Story 1.5 中获取的上下文:
packages/e2e-test-utils/README.mdsrc/index.ts 使用显式命名导出从 Story 1.4 中学到的经验:
从 NFR 提取的性能要求:
单元测试验证:
从 Architecture 文档的错误处理策略:
❌ selectOption failed
Target: 残疾类型
Expected: 视力残疾
Available: 听力残疾, 言语残疾, 肢体残疾
💡 Suggestion: Check if the option value matches exactly
测试必须验证:
E2ETestErrorErrorContext对齐项目 Monorepo 架构:
packages/e2e-test-utils/@d8d/e2e-test-utils@workspace:*tests/unit/ 目录与项目标准对齐:
docs/standards/testing-standards.md 中的测试规范docs/standards/e2e-radix-testing.md 中的 Radix UI E2E 测试标准PRD 来源:
Architecture 来源:
标准文档来源:
Epic 来源:
前一个故事:
Claude (d8d-model) via create-story workflow
实现完成时间: 2026-01-09
代码审查修复 (2026-01-09):
实际实现:
tests/unit/radix-select.test.ts 单元测试文件selectRadixOptionAsync 的网络空闲等待超时应该使用自定义 timeout)测试覆盖的功能:
selectRadixOption:
selectRadixOptionAsync:
实现建议:
vi.fn() 和 vi.mocked() 来 mock Playwright Page 对象关键检查点:
本故事创建的文件:
packages/e2e-test-utils/tests/unit/radix-select.test.ts - Select 工具函数的单元测试本故事修改的文件:
packages/e2e-test-utils/src/radix-select.ts - 修复了 selectRadixOptionAsync 中网络空闲等待使用自定义 timeout 的 bugpackages/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 测试标准