# Story 1.3: 实现静态 Select 工具函数

Status: done

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

## 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<void>`
**And** 选择器策略：`data-testid` → aria-label + role → text content
**And** 自动处理点击触发器、等待选项列表、点击选项
**And** 错误时抛出 `E2ETestError`，包含标签、期望值、可用选项
**And** 操作在 2 秒内完成（NFR8）
**And** 所有导出函数有完整的 JSDoc 注释

## Tasks / Subtasks

- [x] 实现 `src/radix-select.ts` - 静态 Select 工具函数 (AC: 1, 2, 3, 4)
  - [x] 实现 `selectRadixOption(page, label, value)` 主函数
  - [x] 实现触发器查找逻辑（按优先级：testid → ARIA → text）
  - [x] 实现选项查找逻辑（data-value → text content）
  - [x] 实现点击触发器和选项的交互流程
  - [x] 添加错误处理和友好错误消息
  - [x] 为所有导出函数添加完整 JSDoc 注释
- [x] 更新 `src/index.ts` 导出新增函数 (AC: 6)
- [x] 类型检查通过验证 (AC: 7)
  - [x] 运行 `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<void> {
  // 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<void> {
  // 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
<!-- 触发器（点击展开下拉列表） -->
<button
  type="button"
  role="combobox"
  aria-label="残疾类型"
  data-radix-select-trigger=""
  data-state="closed"
>
  <span>残疾类型</span>
  <svg><!-- 下箭头图标 --></svg>
</button>

<!-- 选项列表容器（点击后展开） -->
<div role="listbox" data-radix-select-content="">
  <!-- 单个选项 -->
  <div
    role="option"
    data-value="blind"
    data-state="unchecked"
    aria-selected="false"
  >
    视力残疾
  </div>
  <div
    role="option"
    data-value="hearing"
    data-state="unchecked"
    aria-selected="false"
  >
    听力残疾
  </div>
  <div
    role="option"
    data-value="physical"
    data-state="unchecked"
    aria-selected="false"
  >
    肢体残疾
  </div>
  <div
    role="option"
    data-value="intellectual"
    data-state="unchecked"
    aria-selected="false"
  >
    智力残疾
  </div>
</div>
```

**关键 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
<!-- 推荐方式：添加 data-testid -->
<button
  data-testid="残疾类型-trigger"
  role="combobox"
  aria-label="残疾类型"
>
  残疾类型
</button>
```

### 项目标准对齐

**与项目标准对齐：**
- 遵循 `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


### Agent Model Used (Dev)\n\nClaude (d8d-model) via dev-story workflow

### Agent Model Used (Dev)

Claude (d8d-model) via dev-story workflow
### Debug Log References

### Completion Notes List

- 故事创建时间: 2026-01-08
- 基于 PRD、Architecture、E2E Radix 测试标准文档创建
- 包含完整的实现指南：触发器查找、选项查找、交互流程
- 包含 DOM 结构理解和选择器策略详细说明
- 为后续异步 Select 函数（Story 1.4）提供基础

**实现完成 (2026-01-08):**
- ✅ 创建 `src/radix-select.ts` 实现静态 Select 工具函数
- ✅ 实现主函数 `selectRadixOption(page, label, value)` - 完整的函数签名和实现
- ✅ 实现触发器查找逻辑 - 按优先级尝试 testid → ARIA → text 三种策略
- ✅ 实现选项查找逻辑 - 按优先级尝试 data-value → text content
- ✅ 实现完整的交互流程 - 点击触发器 → 等待列表 → 获取选项 → 点击选项
- ✅ 添加结构化错误处理 - 使用 `throwError` 和 `E2ETestError` 提供友好错误信息
- ✅ 为导出函数添加完整 JSDoc 注释 - 包含 @description, @param, @throws, @example
- ✅ 更新 `src/index.ts` 导出新增函数
- ✅ 类型检查通过 - 使用 `page.locator().allTextContents()` 替代 `page.evaluate()` 避免 DOM 类型问题
- ✅ 使用 `DEFAULT_TIMEOUTS.static` 常量，符合架构要求
- ✅ 遵循 Playwright auto-waiting 机制，无额外显式等待

**技术决策:**
- 使用 `page.locator("[role=option]").allTextContents()` 而非 `page.evaluate()` 避免 TypeScript DOM 类型问题
- 按优先级实现选择器回退策略，提高测试稳定性
- 内部函数标记为 `@internal`，明确 API 边界

### File List

**本故事需要创建/修改的文件：**
- `packages/e2e-test-utils/src/radix-select.ts` - 静态 Select 工具函数（新建）
- `packages/e2e-test-utils/src/index.ts` - 更新导出（修改）
- `packages/e2e-test-utils/tsconfig.json` - 添加 DOM lib（修改）
- `CLAUDE.md` - 项目配置文件（修改）

**相关文件（已在 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 代码审查修复
**代码审查发现并修复了 8 个问题：**

🔴 **HIGH 修复 (2):**
1. ✅ `findTrigger` 错误上下文添加 `expected` 参数 - 修复错误消息不完整问题
2. ✅ 文本选择器从 `:has-text()` 改为 `:text-is()` - 避免部分匹配误选

🟡 **MEDIUM 修复 (6):**
3. ✅ 删除 `findTrigger` 中 3 处冗余的 null 检查 - 简化代码逻辑
4. ✅ 删除 `findAndClickOption` 中冗余的 null 检查 - 减少嵌套层级
5. ✅ 空 catch 块添加 `console.debug` - 改善调试体验
6. ✅ 内部函数添加完整 JSDoc 注释（@param, @returns, @throws）- 符合项目标准
7. ✅ 所有 `waitForSelector` 调用添加 `state: "visible"` - 更安全的等待策略
8. ✅ File List 更新包含 `CLAUDE.md` - 完整文档修改记录

**代码质量改进:**
- 删除冗余代码，提高可读性
- 增强错误处理和调试信息
- 完善文档注释
- 精确文本匹配避免误选

### 2026-01-08 - Story 1.3 实现完成
- ✅ 创建 `src/radix-select.ts` - 静态 Select 工具函数
- ✅ 实现 `selectRadixOption()` 主函数及内部辅助函数
- ✅ 实现完整的选择器回退策略（testid → ARIA → text）
- ✅ 添加完整的 JSDoc 注释和错误处理
- ✅ 更新 `src/index.ts` 导出新增函数
- ✅ 类型检查通过
- ✅ 状态更新为 review

### 2026-01-08 - Story 1.3 创建
- ✅ 故事文档创建完成
- ✅ 包含完整的实现指南
- ✅ 包含 DOM 结构和选择器策略详细说明
- ✅ 状态设置为 ready-for-dev


### 2026-01-08 - Story 1.3 实现
- ✅ 创建 `src/radix-select.ts` 实现静态 Select 工具函数
- ✅ 实现三级选择器策略（testid → ARIA → text）
- ✅ 实现触发器和选项查找逻辑
- ✅ 添加完整 JSDoc 注释
- ✅ 更新 `src/index.ts` 导出新增函数
- ✅ 更新 `tsconfig.json` 添加 DOM lib
- ✅ 类型检查通过
- ✅ 所有任务已完成，状态设置为 review

