فهرست منبع

feat(e2e-test-utils): 创建 Story 1.5 主导出和基础文档

完成 create-story 工作流,创建 Story 1.5 文档并标记为 ready-for-dev。

包含内容:
- 完整的 Story 1.5 文档(审查 index.ts、创建 README)
- 静态 vs 异步 Select 对比说明
- README 创建指南和模板
- Tree-shaking 导出最佳实践

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 هفته پیش
والد
کامیت
57dc049d5c

+ 35 - 13
_bmad-output/implementation-artifacts/1-4-async-select-tool.md

@@ -1,6 +1,6 @@
 # Story 1.4: 实现异步 Select 工具函数
 
-Status: ready-for-dev
+Status: done
 
 <!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
 
@@ -22,16 +22,16 @@ Status: ready-for-dev
 
 ## Tasks / Subtasks
 
-- [ ] 实现 `src/radix-select.ts` - 异步 Select 工具函数 (AC: 1, 2, 3, 4, 5)
-  - [ ] 实现 `selectRadixOptionAsync(page, label, value, options?)` 主函数
-  - [ ] 复用 `findTrigger` 逻辑查找下拉框触发器
-  - [ ] 实现异步选项等待逻辑(网络空闲 + 选项可见)
-  - [ ] 实现可配置超时机制(默认 5000ms)
-  - [ ] 添加超时错误处理(包含超时时间、可能原因)
-  - [ ] 为所有导出函数添加完整 JSDoc 注释
-- [ ] 更新 `src/index.ts` 导出新增函数 (AC: 6)
-- [ ] 类型检查通过验证
-  - [ ] 运行 `pnpm typecheck` 确保无类型错误
+- [x] 实现 `src/radix-select.ts` - 异步 Select 工具函数 (AC: 1, 2, 3, 4, 5)
+  - [x] 实现 `selectRadixOptionAsync(page, label, value, options?)` 主函数
+  - [x] 复用 `findTrigger` 逻辑查找下拉框触发器
+  - [x] 实现异步选项等待逻辑(网络空闲 + 选项可见)
+  - [x] 实现可配置超时机制(默认 5000ms)
+  - [x] 添加超时错误处理(包含超时时间、可能原因)
+  - [x] 为所有导出函数添加完整 JSDoc 注释
+- [x] 更新 `src/index.ts` 导出新增函数 (AC: 6)
+- [x] 类型检查通过验证
+  - [x] 运行 `pnpm typecheck` 确保无类型错误
 
 ## Dev Notes
 
@@ -438,11 +438,33 @@ Claude (d8d-model) via create-story workflow
 - 超时时获取可用选项用于错误提示
 - 遵循 Story 1.3 的代码审查经验(`allTextContents`、`:text-is()`、完整 JSDoc)
 
+**实现完成 (2026-01-09):**
+- ✅ 实现了 `selectRadixOptionAsync(page, label, value, options?)` 函数
+- ✅ 复用了 `findTrigger()` 函数查找下拉框触发器
+- ✅ 实现了 `waitForOptionAndSelect()` 内部函数处理异步等待
+- ✅ 使用重试机制(100ms 间隔)等待选项出现
+- ✅ 支持可配置超时(默认 5000ms)
+- ✅ 支持可选的网络空闲等待(`waitForNetworkIdle`)
+- ✅ 超时错误包含:超时时间、期望值、可用选项、修复建议
+- ✅ 添加了完整的 JSDoc 注释(@description, @param, @throws, @example)
+- ✅ 类型检查通过(`pnpm typecheck`)
+
+**代码审查修复 (2026-01-09):**
+- ✅ 更新 JSDoc 添加 `waitForOption` 和 `waitForNetworkIdle` 参数说明
+- ✅ 将 `waitForNetworkIdle` 默认值改为 `true`(符合 AC 要求)
+- ✅ 修复 `waitForLoadState` 超时配置,使用 `DEFAULT_TIMEOUTS.networkIdle`
+- ✅ `waitForOptionAndSelect` 添加文本匹配回退策略
+- ✅ 添加注释说明网络空闲等待失败后继续执行的原因
+- ✅ 更新 `types.ts` 中 `AsyncSelectOptions` JSDoc 补充完整说明
+- ✅ 更新 `index.ts` 使用显式导出而非通配符
+- ✅ 类型检查通过验证
+
 ### File List
 
-**本故事需要创建/修改的文件:**
+**本故事修改的文件:**
 - `packages/e2e-test-utils/src/radix-select.ts` - 添加异步 Select 工具函数(修改)
-- `packages/e2e-test-utils/src/index.ts` - 更新导出(修改)
+- `packages/e2e-test-utils/src/types.ts` - 更新 AsyncSelectOptions JSDoc(修改)
+- `packages/e2e-test-utils/src/index.ts` - 更新导出方式(修改)
 
 **相关文件(已在 Story 1.1、1.2、1.3 中完成,本故事使用):**
 - `packages/e2e-test-utils/src/types.ts` - 共享类型定义(AsyncSelectOptions 已存在)

+ 531 - 0
_bmad-output/implementation-artifacts/1-5-main-export-docs.md

@@ -0,0 +1,531 @@
+# Story 1.5: 创建主导出和基础文档
+
+Status: ready-for-dev
+
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
+
+## Story
+
+作为测试开发者,
+我想要可以导入工具函数并查看快速入门文档,
+以便快速开始使用工具包。
+
+## Acceptance Criteria
+
+**Given** Select 工具函数已实现(Story 1.3、1.4 完成)
+**When** 创建/更新 `src/index.ts` 和 `README.md`
+**Then** `index.ts` 导出所有公共函数和类型(tree-shakeable)
+**And** README 包含:安装说明、快速入门、Select 使用示例
+**And** README 说明静态 Select 和异步 Select 的区别和使用场景
+**And** 所有导出函数有完整的 JSDoc 注释(已在 Story 1.3、1.4 完成)
+**And** 可以 `import { selectRadixOption } from '@d8d/e2e-test-utils'`
+
+## Tasks / Subtasks
+
+- [ ] 审查 `src/index.ts` 导出结构 (AC: 1)
+  - [ ] 确认使用显式导出(而非通配符)支持 tree-shaking
+  - [ ] 确认导出所有公共类型和函数
+  - [ ] 确认导出顺序合理(类型、错误、常量、工具函数)
+- [ ] 更新 `README.md` 添加 Select 工具使用文档 (AC: 2, 3, 4)
+  - [ ] 在 API 文档章节添加 `selectRadixOption` 完整说明
+  - [ ] 在 API 文档章节添加 `selectRadixOptionAsync` 完整说明
+  - [ ] 添加静态 Select vs 异步 Select 对比说明
+  - [ ] 添加快速入门示例代码
+- [ ] 验证导入语句可用性 (AC: 5)
+  - [ ] 验证 `import { selectRadixOption } from '@d8d/e2e-test-utils'` 可用
+  - [ ] 验证类型提示正常工作
+
+## Dev Notes
+
+### Epic 1 背景
+
+**Epic 1 目标:** 测试开发者可以安装 `@d8d/e2e-test-utils` 包,立即使用 Select 工具测试 Radix UI Select 组件。
+
+**本故事在 Epic 中的位置:** 第五个故事,完成主导出文档和 README,使工具包可以被外部项目使用和理解。
+
+### 当前包状态分析
+
+**已完成的工作(Story 1.1-1.4):**
+
+1. **Story 1.1 - 包基础结构**
+   - ✅ `package.json` 配置(peer dependencies: @playwright/test)
+   - ✅ `tsconfig.json` 严格模式配置
+   - ✅ `vitest.config.ts` 测试配置
+
+2. **Story 1.2 - 类型定义和错误处理**
+   - ✅ `src/types.ts` - BaseOptions, AsyncSelectOptions, ErrorContext
+   - ✅ `src/errors.ts` - E2ETestError 类
+   - ✅ `src/constants.ts` - DEFAULT_TIMEOUTS, SELECTOR_STRATEGIES
+
+3. **Story 1.3 - 静态 Select 工具**
+   - ✅ `selectRadixOption(page, label, value)` 函数
+   - ✅ 完整 JSDoc 注释
+   - ✅ 三层选择器策略(testid → ARIA → text)
+
+4. **Story 1.4 - 异步 Select 工具**
+   - ✅ `selectRadixOptionAsync(page, label, value, options?)` 函数
+   - ✅ 完整 JSDoc 注释
+   - ✅ 网络空闲等待 + 重试机制
+
+**当前 `src/index.ts` 状态:**
+```typescript
+// 当前使用显式导出(正确,支持 tree-shaking)
+export {
+  selectRadixOption,
+  selectRadixOptionAsync
+} from './radix-select';
+
+// 通配符导出类型(也正确)
+export * from './types';
+export * from './errors';
+export * from './constants';
+```
+
+**当前 `README.md` 状态:**
+- ✅ 已有项目简介和特性说明
+- ✅ 已有安装说明
+- ✅ 已有类型定义和错误类文档
+- ❌ **缺少 Select 工具函数使用示例**
+- ❌ **缺少静态 vs 异步 Select 区分说明**
+
+### 本故事的核心任务
+
+**任务 1:审查 index.ts 导出结构**
+
+当前导出结构已经正确:
+- 使用显式命名导出支持 tree-shaking
+- 类型使用通配符导出(可以,因为类型不增加运行时开销)
+- 导出顺序合理
+
+**可能需要的改进(可选):**
+- 如果未来工具函数增多,可以考虑为每个模块添加明确的 re-export
+- 保持当前的简洁性已经足够
+
+**任务 2:更新 README.md 添加 Select 工具文档**
+
+需要在 README 中添加以下内容:
+
+1. **Select 工具使用示例**(添加到"快速入门"或"API 文档"章节)
+2. **静态 vs 异步 Select 对比**(新增对比表格或说明)
+3. **实际使用场景示例**(残疾人管理测试案例)
+
+### 架构约束和模式
+
+**从架构文档中必须遵循的决策:**
+
+**文档结构需求(Architecture - Documentation Requirements):**
+- README 必须包含:项目简介、安装说明、快速入门、API 文档
+- 每个工具函数至少有 1 个实际使用示例
+- 文档说明静态 Select 和异步 Select 的区别和使用场景
+- 新测试开发者可以在 30 分钟内使用工具函数编写第一个测试(NFR37)
+
+**Tree-shaking 需求(Architecture - Bundle Optimization):**
+- 主导出使用命名导出(named exports),不使用默认导出
+- 避免导出整个模块,而是明确指定导出的函数和类型
+- 类型定义可以安全使用通配符导出(不增加运行时开销)
+
+**API 文档标准(Architecture - API Documentation):**
+- 所有公共 API 必须有 JSDoc 注释(已在 Story 1.3、1.4 完成)
+- README 中的示例应与 JSDoc 中的 @example 保持一致
+- 参数说明使用表格或列表格式,清晰展示每个参数的类型和用途
+
+### 技术实现要求
+
+**`src/index.ts` 审查指南:**
+
+**当前导出结构(已正确):**
+```typescript
+/**
+ * @d8d/e2e-test-utils
+ *
+ * E2E 测试工具集 - 专门用于测试 Radix UI 组件的 Playwright 工具函数
+ *
+ * @packageDocumentation
+ */
+
+// 导出类型定义(通配符导出可以,类型不增加运行时开销)
+export * from './types';
+
+// 导出错误类(通配符导出可以)
+export * from './errors';
+
+// 导出常量(通配符导出可以)
+export * from './constants';
+
+// Radix UI Select 工具(显式导出,支持 tree-shaking)
+export {
+  selectRadixOption,
+  selectRadixOptionAsync
+} from './radix-select';
+```
+
+**审查清单:**
+- ✅ 使用显式命名导出(非默认导出)
+- ✅ 工具函数使用显式导出列表(而非 `export * from './radix-select'`)
+- ✅ 类型定义使用通配符导出(可接受,类型不增加运行时开销)
+- ✅ 包级 JSDoc 注释完整
+- ✅ 导出顺序合理(类型 → 错误 → 常量 → 工具函数)
+
+**无需修改当前结构**,除非发现以下问题:
+- ❌ 如果有默认导出,需要改为命名导出
+- ❌ 如果有 `export * from './radix-select'`,需要改为显式导出
+
+**`README.md` 创建指南:**
+
+**必须包含的章节:**
+
+1. **项目简介**(1-2 段话)
+   - 说明工具包的用途
+   - 说明支持的组件类型(Radix UI)
+   - 说明核心价值(简化 E2E 测试)
+
+2. **安装说明**
+   ```bash
+   # 使用 pnpm workspace 协议(Monorepo 内)
+   pnpm add -D @d8d/e2e-test-utils@workspace:*
+
+   # 使用 npm(发布后)
+   npm install --save-dev @d8d/e2e-test-utils
+   ```
+
+3. **快速入门**(本故事核心任务)
+   ```typescript
+   import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
+
+   // 静态 Select 示例
+   await selectRadixOption(page, '残疾类型', '视力残疾');
+
+   // 异步 Select 示例
+   await selectRadixOptionAsync(page, '省份', '广东省');
+   ```
+
+4. **API 文档**(本故事核心任务)
+   - `selectRadixOption` - 静态 Select 工具
+   - `selectRadixOptionAsync` - 异步 Select 工具
+   - 参数表格说明
+   - 使用示例
+
+5. **静态 vs 异步 Select**(本故事核心任务)
+   - 区别对比表格
+   - 使用场景说明
+   - 选择建议
+
+6. **类型定义**(已存在,保持)
+7. **错误处理**(已存在,保持)
+8. **常量**(已存在,保持)
+
+### 与前一个故事的集成
+
+**Story 1.4 已完成的工作:**
+- ✅ 静态 Select 函数 `selectRadixOption()` 已实现并导出
+- ✅ 异步 Select 函数 `selectRadixOptionAsync()` 已实现并导出
+- ✅ 所有导出函数有完整的 JSDoc 注释
+- ✅ 类型定义完整(BaseOptions, AsyncSelectOptions)
+
+**本故事需要做的:**
+- 审查并确认 `src/index.ts` 导出结构正确(可能无需修改)
+- 创建 `README.md` 添加 Select 工具使用文档和示例
+- 区分静态 Select 和异步 Select 的使用场景
+
+### 前一个故事的关键经验
+
+**从 Story 1.3、1.4 中学习到的经验:**
+
+1. **JSDoc 注释标准:**
+   - 使用 `@description` 标签添加详细描述
+   - 使用 `@param` 标签说明每个参数
+   - 使用 `@throws` 标签说明可能抛出的错误
+   - 使用 `@example` 标签提供实际使用示例
+
+2. **错误处理模式:**
+   - 使用 `E2ETestError` 类提供结构化错误信息
+   - 错误消息包含:操作类型、目标、期望值、可用选项、修复建议
+
+3. **选择器策略:**
+   - 优先级:`data-testid` → `aria-label` + `role` → `text content`
+   - 推荐在 Radix 组件上添加 `data-testid` 属性
+
+### 静态 Select vs 异步 Select
+
+| 特性 | 静态 Select (`selectRadixOption`) | 异步 Select (`selectRadixOptionAsync`) |
+|------|----------------------------------|----------------------------------------|
+| **选项加载时机** | 页面加载时已存在 DOM 中 | 点击触发器后 API 加载 |
+| **使用场景** | 枚举类型(残疾类型、性别等) | 动态数据(省份、城市、银行等) |
+| **等待策略** | 立即查找选项 | 等待网络请求 + 选项出现 |
+| **默认超时** | 2000ms | 5000ms |
+| **配置对象** | 无 | `AsyncSelectOptions` |
+| **网络空闲等待** | 不需要 | 默认启用 |
+| **函数签名** | `selectRadixOption(page, label, value)` | `selectRadixOptionAsync(page, label, value, options?)` |
+
+**选择建议:**
+- 如果下拉框选项在页面加载时已存在 → 使用 `selectRadixOption()`
+- 如果下拉框选项需要从 API 动态加载 → 使用 `selectRadixOptionAsync()`
+- 不确定时,可以使用异步版本(会有额外等待,但更稳定)
+
+### README 模板参考
+
+**快速入门章节建议内容:**
+
+```markdown
+## 快速入门
+
+### 安装
+
+\`\`\`bash
+# Monorepo 内使用 workspace 协议
+pnpm add -D @d8d/e2e-test-utils@workspace:*
+
+# 或使用 npm(发布后)
+npm install --save-dev @d8d/e2e-test-utils
+\`\`\`
+
+### 基本使用
+
+\`\`\`typescript
+import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
+
+// 选择静态下拉框选项(如残疾类型)
+await selectRadixOption(page, '残疾类型', '视力残疾');
+
+// 选择异步加载的下拉框选项(如省份)
+await selectRadixOptionAsync(page, '省份', '广东省');
+\`\`\`
+
+### 选择器策略
+
+工具函数按以下优先级查找下拉框触发器:
+1. `data-testid="标签-trigger"` - 推荐,最稳定
+2. `aria-label="标签"` + `role="combobox"` - 无障碍属性
+3. `text="标签"` - 文本匹配(兜底)
+
+**推荐做法:** 在测试代码或应用中为 Radix Select 添加 `data-testid` 属性。
+```
+
+**API 文档章节建议内容:**
+
+```markdown
+## API 文档
+
+### selectRadixOption()
+
+选择静态枚举型 Radix UI Select 选项。
+
+**函数签名:**
+\`\`\`typescript
+function selectRadixOption(
+  page: Page,
+  label: string,
+  value: string
+): Promise<void>
+\`\`\`
+
+**参数:**
+| 参数 | 类型 | 说明 |
+|------|------|------|
+| `page` | `Page` | Playwright Page 对象 |
+| `label` | `string` | 下拉框的标签文本 |
+| `value` | `string` | 要选择的选项值 |
+
+**示例:**
+\`\`\`typescript
+// 选择残疾类型
+await selectRadixOption(page, '残疾类型', '视力残疾');
+
+// 选择性别
+await selectRadixOption(page, '性别', '男');
+\`\`\`
+
+---
+
+### selectRadixOptionAsync()
+
+选择异步加载的 Radix UI Select 选项。
+
+**函数签名:**
+\`\`\`typescript
+function selectRadixOptionAsync(
+  page: Page,
+  label: string,
+  value: string,
+  options?: AsyncSelectOptions
+): Promise<void>
+\`\`\`
+
+**参数:**
+| 参数 | 类型 | 说明 |
+|------|------|------|
+| `page` | `Page` | Playwright Page 对象 |
+| `label` | `string` | 下拉框的标签文本 |
+| `value` | `string` | 要选择的选项值 |
+| `options` | `AsyncSelectOptions` | 可选配置对象 |
+| `options.timeout` | `number` | 超时时间(毫秒),默认 5000 |
+| `options.waitForOption` | `boolean` | 是否等待选项加载,默认 true |
+| `options.waitForNetworkIdle` | `boolean` | 是否等待网络空闲,默认 true |
+
+**示例:**
+\`\`\`typescript
+// 选择省份(使用默认配置)
+await selectRadixOptionAsync(page, '省份', '广东省');
+
+// 选择城市(自定义超时)
+await selectRadixOptionAsync(page, '城市', '深圳市', {
+  timeout: 10000
+});
+
+// 禁用网络空闲等待(网络不稳定时)
+await selectRadixOptionAsync(page, '地区', '华南', {
+  waitForNetworkIdle: false
+});
+\`\`\`
+```
+
+### 项目标准对齐
+
+**与项目标准对齐:**
+- 遵循 `docs/standards/testing-standards.md` 中的测试规范
+- 遵循 `docs/standards/e2e-radix-testing.md` 中的 Radix UI E2E 测试标准
+- 遵循 `docs/standards/coding-standards.md` 中的编码标准
+
+**文档标准:**
+- README 使用 Markdown 格式
+- 代码示例使用 TypeScript 语法高亮
+- 参数说明使用表格格式
+- 添加实际可运行的示例代码
+
+**TypeScript 配置:**
+- 严格模式已启用(`strict: true`)
+- 所有类型定义支持 IDE 自动补全
+- 导入语句支持类型推导
+
+### 性能约束
+
+**从 NFR 提取的性能要求:**
+- **NFR37**: 新测试开发者可以在 30 分钟内使用工具函数编写第一个测试
+- **NFR39**: 函数命名清晰直观,不需要频繁查看文档
+- **NFR40**: 错误消息对新手友好,包含问题诊断和建议修复步骤
+
+**README 设计考虑:**
+- 提供简洁的快速入门章节(5 分钟内理解)
+- 提供完整的 API 参考文档
+- 提供静态 vs 异步 Select 对比说明
+- 提供实际使用场景示例
+
+### 文件结构约束
+
+**必须遵循的文件结构:**
+```
+packages/e2e-test-utils/
+├── src/
+│   ├── index.ts          # 主导出(审查,可能无需修改)
+│   ├── types.ts          # 共享类型定义
+│   ├── errors.ts         # 错误类
+│   ├── constants.ts      # 常量定义
+│   └── radix-select.ts   # Radix UI Select 工具
+├── README.md             # 包文档(本故事创建)
+├── package.json          # 包配置
+├── tsconfig.json         # TypeScript 配置
+└── vitest.config.ts      # 测试配置
+```
+
+**禁止事项(Anti-Patterns):**
+- ❌ 使用默认导出(`export default`)
+- ❌ 在 README 中使用不准确的示例代码
+- ❌ 缺少静态 vs 异步 Select 的区分说明
+- ❌ 示例代码与实际 API 不匹配
+
+### 测试要求
+
+**文档验证方法:**
+- 手动验证 README 中的示例代码可以正确运行
+- 验证导入语句 `import { selectRadixOption } from '@d8d/e2e-test-utils'` 可用
+- 验证类型提示在 IDE 中正常工作
+- 验证快速入门章节可在 30 分钟内完成(NFR37)
+
+**集成测试(在 Epic 3 中实现):**
+- 本故事创建的文档将在 Epic 3 中通过实际 E2E 测试验证
+- 残疾人管理测试将作为完整的使用示例
+
+### 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 - 文档和开发者支持](_bmad-output/planning-artifacts/epics.md#文档和开发者支持-fr33-fr40) - FR33-FR40 需求
+
+**Architecture 来源:**
+- [Architecture - 文档需求](_bmad-output/planning-artifacts/architecture.md#documentation-requirements) - README 和 JSDoc 要求
+- [Architecture - 包结构需求](_bmad-output/planning-artifacts/architecture.md#package-structure-requirements) - 导出规范
+
+**标准文档来源:**
+- [E2E Radix UI 测试标准](docs/standards/e2e-radix-testing.md) - 核心测试标准文档
+- [Project Context](_bmad-output/project-context.md) - 项目技术栈和规则
+
+**Epic 来源:**
+- [Epic 1 - Story 1.5](_bmad-output/planning-artifacts/epics.md#story-15-创建主导出和基础文档) - 原始用户故事和验收标准
+
+**前一个故事:**
+- [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) - 类型、错误、常量
+- [Story 1.3 - 实现静态 Select 工具函数](_bmad-output/implementation-artifacts/1-3-static-select-tool.md) - 静态 Select 函数
+- [Story 1.4 - 实现异步 Select 工具函数](_bmad-output/implementation-artifacts/1-4-async-select-tool.md) - 异步 Select 函数
+
+## 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.3、1.4 的实现状态创建
+- 包含完整的 README 创建指南和模板
+- 区分静态和异步 Select 的使用场景
+- 包含 tree-shaking 导出最佳实践
+
+**实现建议:**
+- `src/index.ts` 当前结构已经正确,可能无需修改
+- 主要任务是创建 `README.md` 添加 Select 工具使用文档
+- README 应包含:安装说明、快速入门、API 文档、静态 vs 异步 Select 对比
+- 确保示例代码与实际 API 一致
+- 确保文档简洁清晰,新开发者可在 30 分钟内上手
+
+**关键检查点:**
+- ✅ `src/index.ts` 使用显式命名导出(支持 tree-shaking)
+- ✅ 所有 Select 函数有完整 JSDoc(已在 Story 1.3、1.4 完成)
+- ❌ README.md 需要创建 Select 工具使用文档
+- ❌ README.md 需要添加静态 vs 异步 Select 对比说明
+
+### File List
+
+**本故事可能修改的文件:**
+- `packages/e2e-test-utils/src/index.ts` - 审查导出结构(可能无需修改)
+- `packages/e2e-test-utils/README.md` - 创建 Select 工具使用文档(主要任务)
+
+**相关文件(已在 Story 1.1-1.4 中完成,本故事使用):**
+- `packages/e2e-test-utils/src/radix-select.ts` - Select 工具函数实现
+- `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-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 测试标准
+

+ 2 - 2
_bmad-output/implementation-artifacts/sprint-status.yaml

@@ -45,8 +45,8 @@ development_status:
   1-1-create-package-structure: done
   1-2-implement-types-errors: done
   1-3-static-select-tool: done
-  1-4-async-select-tool: ready-for-dev
-  1-5-main-export-docs: backlog
+  1-4-async-select-tool: done
+  1-5-main-export-docs: ready-for-dev
   1-6-select-unit-tests: backlog
   epic-1-retrospective: optional
 

+ 4 - 1
packages/e2e-test-utils/src/index.ts

@@ -45,4 +45,7 @@ export * from './errors';
 export * from './constants';
 
 // Radix UI Select 工具
-export * from './radix-select';
+export {
+  selectRadixOption,
+  selectRadixOptionAsync
+} from './radix-select';

+ 159 - 0
packages/e2e-test-utils/src/radix-select.ts

@@ -1,4 +1,5 @@
 import type { Page } from "@playwright/test";
+import type { AsyncSelectOptions } from "./types";
 import { throwError } from "./errors";
 import { DEFAULT_TIMEOUTS } from "./constants";
 
@@ -134,3 +135,161 @@ async function findAndClickOption(
     suggestion: "检查选项值是否正确,或确认选项已加载到 DOM 中"
   });
 }
+
+/**
+ * 选择 Radix UI 下拉框的异步加载选项
+ *
+ * @description
+ * 用于选择通过 API 异步加载的 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 - 要选择的选项值
+ * @param options - 可选配置
+ * @param options.timeout - 超时时间(毫秒),默认 5000ms
+ * @param options.waitForOption - 是否等待选项加载完成(默认:true)
+ * @param options.waitForNetworkIdle - 是否等待网络空闲后再操作(默认:true)
+ * @throws {E2ETestError} 当触发器未找到或等待超时时
+ *
+ * @example
+ * ```ts
+ * // 选择省份(异步加载,默认等待网络空闲)
+ * await selectRadixOptionAsync(page, '省份', '广东省');
+ *
+ * // 选择城市(自定义超时,禁用网络空闲等待)
+ * await selectRadixOptionAsync(page, '城市', '深圳市', {
+ *   timeout: 10000,
+ *   waitForNetworkIdle: false
+ * });
+ * ```
+ */
+export async function selectRadixOptionAsync(
+  page: Page,
+  label: string,
+  value: string,
+  options?: AsyncSelectOptions
+): Promise<void> {
+  // 1. 合并默认配置
+  const config = {
+    timeout: options?.timeout ?? DEFAULT_TIMEOUTS.async,
+    waitForOption: options?.waitForOption ?? true,
+    waitForNetworkIdle: options?.waitForNetworkIdle ?? true
+  };
+
+  // 2. 查找触发器(复用静态 Select 的逻辑)
+  const trigger = await findTrigger(page, label, value);
+
+  // 3. 点击触发器展开选项列表
+  await trigger.click();
+
+  // 4. 等待选项列表容器出现
+  await page.waitForSelector('[role="listbox"]', {
+    timeout: DEFAULT_TIMEOUTS.static,
+    state: 'visible'
+  });
+
+  // 5. 等待网络空闲(处理大量数据加载)
+  // 注意:网络空闲等待失败不会中断流程,因为某些场景下网络可能始终不空闲
+  if (config.waitForNetworkIdle) {
+    try {
+      await page.waitForLoadState('networkidle', { timeout: DEFAULT_TIMEOUTS.networkIdle });
+    } catch (err) {
+      console.debug('网络空闲等待超时,继续尝试选择选项', err);
+    }
+  }
+
+  // 6. 等待选项出现并选择
+  if (config.waitForOption) {
+    await waitForOptionAndSelect(page, value, config.timeout);
+  } else {
+    // 不等待选项,直接尝试选择(向后兼容)
+    const availableOptions = await page.locator('[role="option"]').allTextContents();
+    await findAndClickOption(page, value, availableOptions);
+  }
+}
+
+/**
+ * 等待异步选项加载并完成选择
+ *
+ * @description
+ * 使用重试机制等待异步加载的选项出现在 DOM 中,
+ * 然后完成选择操作。
+ *
+ * 按优先级尝试两种选择器策略:
+ * 1. data-value 属性(精确匹配)
+ * 2. 精确文本匹配(`:text-is()`)
+ *
+ * @internal
+ *
+ * @param page - Playwright Page 对象
+ * @param value - 选项值
+ * @param timeout - 超时时间(毫秒)
+ * @throws {E2ETestError} 当等待超时时
+ */
+async function waitForOptionAndSelect(
+  page: Page,
+  value: string,
+  timeout: number
+): Promise<void> {
+  const startTime = Date.now();
+  const retryInterval = 100; // 重试间隔(毫秒)
+
+  // 等待选项出现(使用重试机制)
+  while (Date.now() - startTime < timeout) {
+    try {
+      // 策略 1: data-value(精确匹配)
+      const dataValueSelector = `[role="option"][data-value="${value}"]`;
+      const option = await page.waitForSelector(dataValueSelector, {
+        timeout: retryInterval,
+        state: 'visible'
+      });
+
+      if (option) {
+        await option.click();
+        return; // 成功选择
+      }
+    } catch {
+      // 选项还未出现,继续尝试下一个策略
+    }
+
+    // 等待一小段时间后重试
+    try {
+      await page.waitForTimeout(retryInterval);
+    } catch {
+      // waitForTimeout 可能被中断,忽略错误继续重试
+    }
+  }
+
+  // 策略 2 失败后,尝试文本匹配策略(一次性尝试,不重试)
+  try {
+    const textSelector = `[role="option"]:text-is("${value}")`;
+    const option = await page.waitForSelector(textSelector, {
+      timeout: 1000,
+      state: 'visible'
+    });
+
+    if (option) {
+      await option.click();
+      return; // 成功选择
+    }
+  } catch {
+    // 文本策略也失败,继续抛出错误
+  }
+
+  // 超时:获取当前可用的选项用于错误提示
+  const availableOptions = await page.locator('[role="option"]').allTextContents();
+
+  throwError({
+    operation: 'selectRadixOptionAsync',
+    target: `选项 "${value}"`,
+    expected: `在 ${timeout}ms 内加载`,
+    available: availableOptions,
+    suggestion: '检查网络请求是否正常,或增加超时时间'
+  });
+}

+ 11 - 3
packages/e2e-test-utils/src/types.ts

@@ -55,18 +55,26 @@ export interface ErrorContext {
  * @description
  * 用于异步加载选项的 Radix UI Select 组件。
  *
+ * @property {number} timeout - 超时时间(毫秒),默认 5000ms
+ * @property {boolean} waitForOption - 是否等待选项加载完成,默认 true
+ * @property {boolean} waitForNetworkIdle - 是否等待网络空闲后再操作,默认 true
+ *
  * @example
  * ```ts
- * await selectRadixOptionAsync(page, '省份', '广东省', {
+ * // 默认配置(等待网络空闲和选项加载)
+ * await selectRadixOptionAsync(page, '省份', '广东省');
+ *
+ * // 自定义超时并禁用网络空闲等待
+ * await selectRadixOptionAsync(page, '城市', '深圳市', {
  *   timeout: 10000,
- *   waitForOption: true
+ *   waitForNetworkIdle: false
  * });
  * ```
  */
 export interface AsyncSelectOptions extends BaseOptions {
   /** 是否等待选项加载完成(默认:true)*/
   waitForOption?: boolean;
-  /** 等待网络空闲后再操作(默认:false)*/
+  /** 等待网络空闲后再操作(默认:true)*/
   waitForNetworkIdle?: boolean;
 }