# Story 1.3: 实现静态 Select 工具函数 Status: ready-for-dev ## Story 作为测试开发者, 我想要使用 `selectRadixOption()` 函数选择静态枚举型下拉框, 以便无需理解 Radix UI DOM 结构就能编写测试。 ## Acceptance Criteria **Given** 类型定义和错误处理已创建(Story 1.1、1.2 完成) **When** 实现 `src/radix-select.ts` 中的 `selectRadixOption()` 函数 **Then** 函数签名:`selectRadixOption(page: Page, label: string, value: string): Promise` **And** 选择器策略:`data-testid` → aria-label + role → text content **And** 自动处理点击触发器、等待选项列表、点击选项 **And** 错误时抛出 `E2ETestError`,包含标签、期望值、可用选项 **And** 操作在 2 秒内完成(NFR8) **And** 所有导出函数有完整的 JSDoc 注释 ## Tasks / Subtasks - [ ] 实现 `src/radix-select.ts` - 静态 Select 工具函数 (AC: 1, 2, 3, 4) - [ ] 实现 `selectRadixOption(page, label, value)` 主函数 - [ ] 实现触发器查找逻辑(按优先级:testid → ARIA → text) - [ ] 实现选项查找逻辑(data-value → text content) - [ ] 实现点击触发器和选项的交互流程 - [ ] 添加错误处理和友好错误消息 - [ ] 为所有导出函数添加完整 JSDoc 注释 - [ ] 更新 `src/index.ts` 导出新增函数 (AC: 6) - [ ] 类型检查通过验证 (AC: 7) - [ ] 运行 `pnpm typecheck` 确保无类型错误 ## Dev Notes ### Epic 1 背景 **Epic 1 目标:** 测试开发者可以安装 `@d8d/e2e-test-utils` 包,立即使用 Select 工具测试 Radix UI Select 组件。 **本故事在 Epic 中的位置:** 第三个故事,实现核心的静态 Select 工具函数。这是第一个实际可用的工具函数,也是后续异步 Select 函数的基础。 ### 架构约束和模式 **从架构文档中必须遵循的决策:** **选择器策略:混合策略(testid → ARIA → 文本)** - 优先级 1: `data-testid="${label}-trigger"` - 最稳定,推荐 - 优先级 2: `aria-label="${label}"` + `role="combobox"` - 无障碍属性 - 优先级 3: `text="${label}"` - 文本匹配兜底 **API 设计模式:3个必需参数 + 可选配置对象** - 必需参数:`page`, `label`, `value` - 静态 Select 不需要配置对象(保持简洁) - 异步 Select 需要配置对象(在 Story 1.4 实现) **错误处理策略:结构化错误类 + 友好消息** - 使用 `E2ETestError` 类(已在 Story 1.2 实现) - 包含 `ErrorContext` 信息(operation, target, expected, available, suggestion) - 错误消息格式:❌ 操作失败、上下文信息、💡 修复建议 ### 技术实现要求 **`src/radix-select.ts` - 静态 Select 工具函数实现指南:** ```typescript import type { Page } from '@playwright/test'; import { throwError } from './errors'; import { DEFAULT_TIMEOUTS } from './constants'; /** * 选择 Radix UI 下拉框的静态选项 * * @description * 自动处理 Radix UI Select 的 DOM 结构和交互流程, * 无需手动理解组件的复杂结构。 * * 支持的选择器策略(按优先级): * 1. `data-testid="${label}-trigger"` - 推荐,最稳定 * 2. `aria-label="${label}"` + `role="combobox"` - 无障碍属性 * 3. `text="${label}"` - 文本匹配(兜底) * * @param page - Playwright Page 对象 * @param label - 下拉框的标签文本(用于定位触发器) * @param value - 要选择的选项值 * @throws {E2ETestError} 当触发器或选项未找到时 * * @example * ```ts * // 选择残疾类型 * await selectRadixOption(page, '残疾类型', '视力残疾'); * * // 选择性别 * await selectRadixOption(page, '性别', '男'); * * // 选择残疾等级 * await selectRadixOption(page, '残疾等级', '一级'); * ``` */ export async function selectRadixOption( page: Page, label: string, value: string ): Promise { // TODO: 实现触发器查找逻辑 // TODO: 实现选项查找和点击逻辑 // TODO: 添加错误处理 } ``` **触发器查找逻辑实现:** ```typescript /** * 查找 Radix UI Select 触发器 * * @internal * * @param page - Playwright Page 对象 * @param label - 下拉框标签 * @returns 触发器元素 * @throws {E2ETestError} 当触发器未找到时 */ async function findTrigger(page: Page, label: string) { const timeout = DEFAULT_TIMEOUTS.static; // 策略 1: data-testid const testIdSelector = `[data-testid="${label}-trigger"]`; try { const trigger = await page.waitForSelector(testIdSelector, { timeout }); if (trigger) return trigger; } catch { // 继续尝试下一个策略 } // 策略 2: aria-label + role try { const ariaSelector = `[aria-label="${label}"][role="combobox"]`; const trigger = await page.waitForSelector(ariaSelector, { timeout }); if (trigger) return trigger; } catch { // 继续尝试下一个策略 } // 策略 3: text content try { const trigger = await page.waitForSelector(`text="${label}"`, { timeout }); if (trigger) return trigger; } catch { // 所有策略都失败 } // 未找到触发器 throwError({ operation: 'selectRadixOption', target: label, expected: value, suggestion: '检查下拉框标签是否正确,或添加 data-testid 属性' }); } ``` **选项查找和点击逻辑实现:** ```typescript /** * 查找并点击 Radix UI Select 选项 * * @internal * * @param page - Playwright Page 对象 * @param value - 选项值 * @param availableOptions - 可用选项列表(用于错误提示) * @throws {E2ETestError} 当选项未找到时 */ async function findAndClickOption( page: Page, value: string, availableOptions: string[] ) { const timeout = DEFAULT_TIMEOUTS.static; // 策略 1: data-value const dataValueSelector = `[role="option"][data-value="${value}"]`; try { const option = await page.waitForSelector(dataValueSelector, { timeout }); if (option) { await option.click(); return; } } catch { // 继续尝试下一个策略 } // 策略 2: text content const textSelector = `[role="option"]:has-text("${value}")`; try { const option = await page.waitForSelector(textSelector, { timeout }); if (option) { await option.click(); return; } } catch { // 所有策略都失败 } // 未找到选项 throwError({ operation: 'selectRadixOption', target: `选项 "${value}"`, available: availableOptions, suggestion: '检查选项值是否正确,或确认选项已加载到 DOM 中' }); } ``` **主函数完整实现:** ```typescript export async function selectRadixOption( page: Page, label: string, value: string ): Promise { // 1. 查找触发器 const trigger = await findTrigger(page, label); // 2. 点击触发器展开选项列表 await trigger.click(); // 3. 等待选项列表出现 await page.waitForSelector('[role="listbox"]', { timeout: DEFAULT_TIMEOUTS.static }); // 4. 获取可用选项列表(用于错误提示) const availableOptions = await page.evaluate(() => { const options = document.querySelectorAll('[role="option"]'); return Array.from(options).map(opt => opt.textContent || ''); }); // 5. 查找并点击选项 await findAndClickOption(page, value, availableOptions); // 6. 等待下拉框关闭(可选,确保操作完成) // 不需要显式等待,Playwright auto-waiting 会处理 } ``` **`src/index.ts` - 更新主导出文件:** ```typescript // ... 现有导出 ... // Radix UI Select 工具 export { selectRadixOption } from './radix-select'; ``` ### 与前一个故事的集成 **Story 1.1 已完成的工作:** - ✅ 包结构已创建:`packages/e2e-test-utils/` - ✅ 配置文件已就绪:`package.json`, `tsconfig.json`, `vitest.config.ts` - ✅ 目录结构已创建:`tests/unit/`, `tests/fixtures/` **Story 1.2 已完成的工作:** - ✅ 类型定义完整:`src/types.ts` 包含所有共享类型 - ✅ 错误处理完整:`src/errors.ts` 包含 `E2ETestError` 和 `throwError` - ✅ 常量定义完整:`src/constants.ts` 包含 `DEFAULT_TIMEOUTS` 和 `SELECTOR_STRATEGIES` **本故事需要做的:** - 创建 `src/radix-select.ts` 实现静态 Select 工具函数 - 更新 `src/index.ts` 导出新函数 - 运行 `pnpm typecheck` 验证类型检查通过 ### DOM 结构理解 **Radix UI Select 的典型 DOM 结构:** ```html
视力残疾
听力残疾
肢体残疾
智力残疾
``` **关键 DOM 属性:** - 触发器:`role="combobox"`, `data-radix-select-trigger` - 选项列表:`role="listbox"`, `data-radix-select-content` - 单个选项:`role="option"`, `data-value`(选项值) ### Radix UI 选择器策略实现细节 **从 E2E Radix 测试标准文档中提取的实现细节:** ```typescript // 触发器选择器优先级 const TRIGGER_SELECTORS = [ `[data-testid="${label}-trigger"]`, // 最高优先级 `text="${label}"`, // 文本匹配 `[role="combobox"]` // 兜底 ]; // 选项选择器优先级 const OPTION_SELECTORS = [ `[role="option"][data-value="${value}"]`, // data-value + role `[role="option"]:has-text("${value}")` // 文本匹配兜底 ]; ``` **推荐在应用代码中添加 data-testid:** ```html ``` ### 项目标准对齐 **与项目标准对齐:** - 遵循 `docs/standards/testing-standards.md` 中的测试规范 - 遵循 `docs/standards/e2e-radix-testing.md` 中的 Radix UI E2E 测试标准 - 遵循 `docs/standards/coding-standards.md` 中的编码标准 **TypeScript 配置:** - 严格模式已启用(`strict: true`),禁止 `any` 类型 - 所有函数参数和返回值必须有明确类型注解 **JSDoc 注释标准:** - 所有导出函数必须有完整 JSDoc - 使用 `@param`, `@throws`, `@example` 标签 - 添加实际使用示例代码 **命名约定:** - 函数:camelCase,动词+名词(如 `selectRadixOption`) - 内部函数:camelCase,动词开头(如 `findTrigger`, `findAndClickOption`) - 私有函数使用 `@internal` JSDoc 标记 ### 性能约束 **从 NFR 提取的性能要求:** - **NFR8**: 静态 Select 选择操作应在 2 秒内完成(目标 < 1s) - **NFR10**: 工具函数本身的开销不超过 100ms(不包括 Playwright 操作时间) - **NFR12**: 静态选项使用合理的默认超时(2 秒),避免不必要的等待 - **NFR14**: 工具函数使用 Playwright 的 auto-waiting 机制,减少显式等待的需要 **实现时需要考虑:** - 使用 `DEFAULT_TIMEOUTS.static` (2000ms) 而不是硬编码 - 优先使用 Playwright 的 `waitForSelector` 而不是 `waitForTimeout` - 不添加额外的显式等待(让 auto-waiting 处理) ### 文件结构约束 **必须遵循的文件结构:** ``` packages/e2e-test-utils/src/ ├── index.ts # 主导出(需要更新) ├── types.ts # 共享类型定义(已完成) ├── errors.ts # 错误类(已完成) ├── constants.ts # 常量定义(已完成) └── radix-select.ts # Radix UI Select 工具(本故事创建) ``` **禁止事项(Anti-Patterns):** - ❌ 使用 `any` 类型 - ❌ 硬编码超时值(必须使用 `DEFAULT_TIMEOUTS.static`) - ❌ 抛出原生 `Error`(必须使用 `throwError` 辅助函数) - ❌ 缺少 JSDoc 注释 - ❌ 使用 `waitForTimeout`(应该依赖 auto-waiting) - ❌ 只实现单一选择器策略(必须按优先级尝试多种策略) ### 测试要求 **单元测试(在 Story 1.6 中实现):** 本故事创建的 `selectRadixOption` 函数将在 Story 1.6 中进行单元测试。 **当前验证方法:** - 类型检查:`pnpm typecheck` - JSDoc 验证:手动检查所有导出都有完整注释 ### Project Structure Notes **对齐项目 Monorepo 架构:** - 包位于 `packages/e2e-test-utils/` - 使用 workspace 协议安装:`@d8d/e2e-test-utils@workspace:*` - 与现有 `@d8d/shared-test-util`(后端集成测试)分离 **与项目标准对齐:** - 遵循 `docs/standards/testing-standards.md` 中的测试规范 - 遵循 `docs/standards/web-ui-testing-standards.md` 中的 Web UI 测试规范 - 遵循 `docs/standards/e2e-radix-testing.md` 中的 Radix UI E2E 测试标准(核心标准文档) ### References **PRD 来源:** - [PRD - E2E测试工具包](_bmad-output/planning-artifacts/prd.md) - 项目需求概述 - [PRD - Radix UI Select 测试需求](_bmad-output/planning-artifacts/epics.md#radix-ui-select-测试支持-fr1-fr6) - FR1-FR6 需求 **Architecture 来源:** - [Architecture - 包结构策略](_bmad-output/planning-artifacts/architecture.md#package-structure) - 按功能分组 - [Architecture - API 设计模式](_bmad-output/planning-artifacts/architecture.md#api-design-pattern) - 3参数+配置对象 - [Architecture - 选择器策略](_bmad-output/planning-artifacts/architecture.md#selector-strategy) - 混合策略 - [Architecture - 错误处理策略](_bmad-output/planning-artifacts/architecture.md#error-handling-strategy) - 结构化错误类 - [Architecture - 实现模式](_bmad-output/planning-artifacts/architecture.md#implementation-patterns--consistency-rules) - 命名和格式约定 - [Architecture - 项目结构](_bmad-output/planning-artifacts/architecture.md#project-structure--boundaries) - 完整目录结构 **标准文档来源:** - [E2E Radix UI 测试标准](docs/standards/e2e-radix-testing.md) - 核心测试标准文档,包含 DOM 结构和选择器策略 - [Project Context](_bmad-output/project-context.md) - 项目技术栈和规则 **Epic 来源:** - [Epic 1 - Story 1.3](_bmad-output/planning-artifacts/epics.md#story-13-实现静态-select-工具函数) - 原始用户故事和验收标准 **前一个故事:** - [Story 1.1 - 创建包基础结构和配置](_bmad-output/implementation-artifacts/1-1-create-package-structure.md) - 包基础设施 - [Story 1.2 - 实现类型定义和错误处理](_bmad-output/implementation-artifacts/1-2-implement-types-errors.md) - 类型、错误、常量 ## Dev Agent Record ### Agent Model Used Claude (d8d-model) via create-story workflow ### Debug Log References ### Completion Notes List - 故事创建时间: 2026-01-08 - 基于 PRD、Architecture、E2E Radix 测试标准文档创建 - 包含完整的实现指南:触发器查找、选项查找、交互流程 - 包含 DOM 结构理解和选择器策略详细说明 - 为后续异步 Select 函数(Story 1.4)提供基础 ### File List **本故事需要创建/修改的文件:** - `packages/e2e-test-utils/src/radix-select.ts` - 静态 Select 工具函数(新建) - `packages/e2e-test-utils/src/index.ts` - 更新导出(修改) **相关文件(已在 Story 1.1、1.2 中完成,本故事使用):** - `packages/e2e-test-utils/src/types.ts` - 共享类型定义 - `packages/e2e-test-utils/src/errors.ts` - 错误类和错误处理 - `packages/e2e-test-utils/src/constants.ts` - 超时和选择器策略常量 **只读参考文件:** - `_bmad-output/implementation-artifacts/1-1-create-package-structure.md` - 前一个故事 - `_bmad-output/implementation-artifacts/1-2-implement-types-errors.md` - 前一个故事 - `_bmad-output/planning-artifacts/epics.md` - Epic 和故事定义 - `_bmad-output/planning-artifacts/architecture.md` - 架构决策和模式 - `docs/standards/e2e-radix-testing.md` - E2E Radix UI 测试标准 ## Change Log ### 2026-01-08 - Story 1.3 创建 - ✅ 故事文档创建完成 - ✅ 包含完整的实现指南 - ✅ 包含 DOM 结构和选择器策略详细说明 - ✅ 状态设置为 ready-for-dev