|
|
@@ -0,0 +1,458 @@
|
|
|
+# Story 1.4: 实现异步 Select 工具函数
|
|
|
+
|
|
|
+Status: ready-for-dev
|
|
|
+
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
+
|
|
|
+## Story
|
|
|
+
|
|
|
+作为测试开发者,
|
|
|
+我想要使用 `selectRadixOptionAsync()` 函数选择异步加载的下拉框,
|
|
|
+以便测试省份、城市等动态加载的选项。
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+
|
|
|
+**Given** 静态 Select 函数已实现(Story 1.3 完成)
|
|
|
+**When** 实现 `selectRadixOptionAsync(page, label, value, options?)` 函数
|
|
|
+**Then** 支持 `AsyncSelectOptions` 配置(timeout, waitForOption)
|
|
|
+**And** 使用 `waitForLoadState('networkidle')` 等待异步加载
|
|
|
+**And** 默认超时 5 秒,可配置
|
|
|
+**And** 超时时提供清晰错误消息(标签、期望值、超时时间、可能原因)
|
|
|
+**And** 所有导出函数有完整的 JSDoc 注释
|
|
|
+
|
|
|
+## 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` 确保无类型错误
|
|
|
+
|
|
|
+## Dev Notes
|
|
|
+
|
|
|
+### Epic 1 背景
|
|
|
+
|
|
|
+**Epic 1 目标:** 测试开发者可以安装 `@d8d/e2e-test-utils` 包,立即使用 Select 工具测试 Radix UI Select 组件。
|
|
|
+
|
|
|
+**本故事在 Epic 中的位置:** 第四个故事,实现异步 Select 工具函数。这是在静态 Select 基础上的扩展,处理动态加载选项的场景。
|
|
|
+
|
|
|
+### 架构约束和模式
|
|
|
+
|
|
|
+**从架构文档中必须遵循的决策:**
|
|
|
+
|
|
|
+**API 设计模式:4 个参数(包含可选配置对象)**
|
|
|
+- 必需参数:`page`, `label`, `value`
|
|
|
+- 可选参数:`options?: AsyncSelectOptions`
|
|
|
+- 与静态 Select 保持一致的接口风格
|
|
|
+
|
|
|
+**类型定义:已在 Story 1.2 中定义**
|
|
|
+```typescript
|
|
|
+export interface AsyncSelectOptions extends BaseOptions {
|
|
|
+ /** 是否等待选项加载完成(默认:true)*/
|
|
|
+ waitForOption?: boolean;
|
|
|
+ /** 等待网络空闲后再操作(默认:false)*/
|
|
|
+ waitForNetworkIdle?: boolean;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**错误处理策略:超时错误需要特殊处理**
|
|
|
+- 超时错误应包含:超时时间、期望值、可能原因
|
|
|
+- 区分"选项未找到"和"等待超时"两种错误场景
|
|
|
+- 使用 `E2ETestError` 类保持错误格式一致
|
|
|
+
|
|
|
+### 技术实现要求
|
|
|
+
|
|
|
+**`src/radix-select.ts` - 异步 Select 工具函数实现指南:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+/**
|
|
|
+ * 选择 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 - 可选配置
|
|
|
+ * @throws {E2ETestError} 当触发器未找到或等待超时时
|
|
|
+ *
|
|
|
+ * @example
|
|
|
+ * ```ts
|
|
|
+ * // 选择省份(异步加载)
|
|
|
+ * await selectRadixOptionAsync(page, '省份', '广东省');
|
|
|
+ *
|
|
|
+ * // 选择城市(自定义超时)
|
|
|
+ * await selectRadixOptionAsync(page, '城市', '深圳市', {
|
|
|
+ * timeout: 10000,
|
|
|
+ * waitForNetworkIdle: true
|
|
|
+ * });
|
|
|
+ * ```
|
|
|
+ */
|
|
|
+export async function selectRadixOptionAsync(
|
|
|
+ page: Page,
|
|
|
+ label: string,
|
|
|
+ value: string,
|
|
|
+ options?: AsyncSelectOptions
|
|
|
+): Promise<void> {
|
|
|
+ // TODO: 实现异步选项选择逻辑
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**异步等待策略实现:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+/**
|
|
|
+ * 等待异步选项加载并完成选择
|
|
|
+ *
|
|
|
+ * @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();
|
|
|
+
|
|
|
+ // 等待选项出现(使用重试机制)
|
|
|
+ while (Date.now() - startTime < timeout) {
|
|
|
+ try {
|
|
|
+ // 尝试查找选项
|
|
|
+ const option = await page.waitForSelector(
|
|
|
+ `[role="option"][data-value="${value}"]`,
|
|
|
+ { timeout: 1000, state: 'visible' }
|
|
|
+ );
|
|
|
+
|
|
|
+ if (option) {
|
|
|
+ await option.click();
|
|
|
+ return; // 成功选择
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ // 选项还未出现,继续等待
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待一小段时间后重试
|
|
|
+ await page.waitForTimeout(100);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 超时:获取当前可用的选项用于错误提示
|
|
|
+ const availableOptions = await page.locator('[role="option"]').allTextContents();
|
|
|
+
|
|
|
+ throwError({
|
|
|
+ operation: 'selectRadixOptionAsync',
|
|
|
+ target: `选项 "${value}"`,
|
|
|
+ expected: `在 ${timeout}ms 内加载`,
|
|
|
+ available: availableOptions,
|
|
|
+ suggestion: '检查网络请求是否正常,或增加超时时间'
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**主函数完整实现:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+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 ?? false
|
|
|
+ };
|
|
|
+
|
|
|
+ // 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: config.timeout });
|
|
|
+ } catch (err) {
|
|
|
+ console.debug('网络空闲等待超时,继续尝试选择选项', err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 等待选项出现并选择
|
|
|
+ await waitForOptionAndSelect(page, value, config.timeout);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 与前一个故事的集成
|
|
|
+
|
|
|
+**Story 1.3 已完成的工作:**
|
|
|
+- ✅ 静态 Select 函数 `selectRadixOption()` 已实现
|
|
|
+- ✅ 触发器查找逻辑 `findTrigger()` 已实现
|
|
|
+- ✅ 选项查找逻辑 `findAndClickOption()` 已实现
|
|
|
+- ✅ 选择器策略(testid → ARIA → text)已实现
|
|
|
+- ✅ 错误处理和 JSDoc 注释模式已建立
|
|
|
+
|
|
|
+**本故事需要做的:**
|
|
|
+- 复用 `findTrigger()` 函数查找下拉框触发器
|
|
|
+- 实现 `waitForOptionAndSelect()` 函数处理异步等待
|
|
|
+- 实现 `selectRadixOptionAsync()` 主函数
|
|
|
+- 处理网络空闲等待(`waitForLoadState`)
|
|
|
+- 更新 `src/index.ts` 导出新函数
|
|
|
+
|
|
|
+### 前一个故事的关键经验
|
|
|
+
|
|
|
+**从 Story 1.3 代码审查中学习到的经验:**
|
|
|
+
|
|
|
+1. **DOM 类型问题解决:** 使用 `page.locator().allTextContents()` 替代 `page.evaluate()` 避免 TypeScript DOM 类型问题
|
|
|
+
|
|
|
+2. **精确文本匹配:** 选项选择器使用 `:text-is()` 而非 `:has-text()` 避免部分匹配误选
|
|
|
+
|
|
|
+3. **完整的 JSDoc 注释:** 内部函数需要完整的 JSDoc(@param, @returns, @throws)
|
|
|
+
|
|
|
+4. **空 catch 块处理:** 添加 `console.debug` 改善调试体验
|
|
|
+
|
|
|
+5. **等待策略优化:** 所有 `waitForSelector` 调用添加 `state: "visible"`
|
|
|
+
|
|
|
+6. **错误上下文完整性:** 确保错误上下文包含所有必要参数
|
|
|
+
|
|
|
+### 异步 Select 与静态 Select 的区别
|
|
|
+
|
|
|
+| 特性 | 静态 Select | 异步 Select |
|
|
|
+|------|------------|------------|
|
|
|
+| **选项加载时机** | 页面加载时已存在 | 点击触发器后 API 加载 |
|
|
|
+| **等待策略** | 立即查找选项 | 等待网络请求 + 选项出现 |
|
|
|
+| **默认超时** | 2000ms | 5000ms |
|
|
|
+| **错误类型** | 选项未找到 | 等待超时 |
|
|
|
+| **配置对象** | 无 | `AsyncSelectOptions` |
|
|
|
+
|
|
|
+### DOM 结构理解
|
|
|
+
|
|
|
+**异步 Select 的 DOM 结构与静态 Select 相同:**
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 触发器 -->
|
|
|
+<button
|
|
|
+ data-testid="省份-trigger"
|
|
|
+ role="combobox"
|
|
|
+ aria-label="省份"
|
|
|
+>
|
|
|
+ 省份
|
|
|
+</button>
|
|
|
+
|
|
|
+<!-- 选项列表(初始为空或显示加载状态) -->
|
|
|
+<div role="listbox">
|
|
|
+ <!-- API 请求完成后动态添加选项 -->
|
|
|
+ <div role="option" data-value="guangdong">广东省</div>
|
|
|
+ <div role="option" data-value="beijing">北京市</div>
|
|
|
+ <div role="option" data-value="shanghai">上海市</div>
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+**关键区别:**
|
|
|
+- 选项不是一开始就存在于 DOM 中
|
|
|
+- 需要等待 API 请求完成后选项才出现
|
|
|
+- 可能需要处理加载状态显示
|
|
|
+
|
|
|
+### Playwright API 参考
|
|
|
+
|
|
|
+**等待网络空闲:**
|
|
|
+```typescript
|
|
|
+// 等待网络空闲(所有网络请求完成)
|
|
|
+await page.waitForLoadState('networkidle', { timeout: 5000 });
|
|
|
+
|
|
|
+// 等待特定类型的加载状态
|
|
|
+await page.waitForLoadState('domcontentloaded'); // DOM 加载完成
|
|
|
+await page.waitForLoadState('load'); // 页面 load 事件
|
|
|
+await page.waitForLoadState('networkidle'); // 网络空闲(至少 500ms 无网络活动)
|
|
|
+```
|
|
|
+
|
|
|
+**重试模式:**
|
|
|
+```typescript
|
|
|
+// 使用重试机制等待异步元素
|
|
|
+const maxRetries = 10;
|
|
|
+const retryDelay = 500;
|
|
|
+
|
|
|
+for (let i = 0; i < maxRetries; i++) {
|
|
|
+ try {
|
|
|
+ const element = await page.waitForSelector(selector, { timeout: 1000 });
|
|
|
+ await element.click();
|
|
|
+ break; // 成功,退出循环
|
|
|
+ } catch {
|
|
|
+ if (i < maxRetries - 1) {
|
|
|
+ await page.waitForTimeout(retryDelay);
|
|
|
+ } else {
|
|
|
+ throw new Error('重试失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 项目标准对齐
|
|
|
+
|
|
|
+**与项目标准对齐:**
|
|
|
+- 遵循 `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` 标签
|
|
|
+- 添加实际使用示例代码
|
|
|
+- 内部函数使用 `@internal` 标记
|
|
|
+
|
|
|
+**命名约定:**
|
|
|
+- 函数:camelCase,动词+名词,异步函数添加 `Async` 后缀
|
|
|
+- 内部函数:camelCase,动词开头(如 `waitForOptionAndSelect`)
|
|
|
+- 私有函数使用 `@internal` JSDoc 标记
|
|
|
+
|
|
|
+### 性能约束
|
|
|
+
|
|
|
+**从 NFR 提取的性能要求:**
|
|
|
+- **NFR9**: 异步 Select 选择操作应在 5 秒内完成(默认超时)
|
|
|
+- **NFR10**: 工具函数本身的开销不超过 100ms(不包括 Playwright 操作时间)
|
|
|
+- **NFR13**: 异步选项提供可配置的超时参数,默认值为 5 秒
|
|
|
+- **NFR14**: 工具函数使用 Playwright 的 auto-waiting 机制,减少显式等待的需要
|
|
|
+
|
|
|
+**实现时需要考虑:**
|
|
|
+- 使用 `DEFAULT_TIMEOUTS.async` (5000ms) 作为默认超时
|
|
|
+- 支持自定义超时配置
|
|
|
+- 使用重试机制而非一次性 `waitForTimeout`
|
|
|
+- 优先使用 Playwright 的 `waitForSelector` 而不是 `waitForTimeout`
|
|
|
+
|
|
|
+### 文件结构约束
|
|
|
+
|
|
|
+**必须遵循的文件结构:**
|
|
|
+```
|
|
|
+packages/e2e-test-utils/src/
|
|
|
+├── index.ts # 主导出(需要更新)
|
|
|
+├── types.ts # 共享类型定义(已完成,AsyncSelectOptions 已存在)
|
|
|
+├── errors.ts # 错误类(已完成)
|
|
|
+├── constants.ts # 常量定义(已完成,DEFAULT_TIMEOUTS.async 已存在)
|
|
|
+└── radix-select.ts # Radix UI Select 工具(本故事修改,添加异步函数)
|
|
|
+```
|
|
|
+
|
|
|
+**禁止事项(Anti-Patterns):**
|
|
|
+- ❌ 使用 `any` 类型
|
|
|
+- ❌ 硬编码超时值(必须使用 `DEFAULT_TIMEOUTS.async`)
|
|
|
+- ❌ 抛出原生 `Error`(必须使用 `throwError` 辅助函数)
|
|
|
+- ❌ 缺少 JSDoc 注释
|
|
|
+- ❌ 使用无限 `while(true)` 循环(必须有超时限制)
|
|
|
+- ❌ 使用 `page.waitForTimeout(5000)` 作为主要等待策略(应该使用重试机制)
|
|
|
+
|
|
|
+### 测试要求
|
|
|
+
|
|
|
+**单元测试(在 Story 1.6 中实现):**
|
|
|
+本故事创建的 `selectRadixOptionAsync` 函数将在 Story 1.6 中进行单元测试。
|
|
|
+
|
|
|
+**当前验证方法:**
|
|
|
+- 类型检查:`pnpm typecheck`
|
|
|
+- JSDoc 验证:手动检查所有导出都有完整注释
|
|
|
+- 手动测试:在真实 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 - Radix UI Select 测试需求](_bmad-output/planning-artifacts/epics.md#radix-ui-select-测试支持-fr1-fr6) - FR1-FR6 需求
|
|
|
+
|
|
|
+**Architecture 来源:**
|
|
|
+- [Architecture - API 设计模式](_bmad-output/planning-artifacts/architecture.md#api-design-pattern) - 4参数+配置对象
|
|
|
+- [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#performance-constraints) - NFR8-NFR14
|
|
|
+- [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.4](_bmad-output/planning-artifacts/epics.md#story-14-实现异步-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) - 类型、错误、常量
|
|
|
+- [Story 1.3 - 实现静态 Select 工具函数](_bmad-output/implementation-artifacts/1-3-static-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-08
|
|
|
+- 基于 PRD、Architecture、E2E Radix 测试标准文档创建
|
|
|
+- 基于 Story 1.3 的经验教训创建(代码审查发现的问题)
|
|
|
+- 包含完整的异步等待策略实现指南
|
|
|
+- 包含与前一个故事的集成说明
|
|
|
+- 区分异步和静态 Select 的关键差异
|
|
|
+
|
|
|
+**实现建议:**
|
|
|
+- 复用 `findTrigger` 函数查找触发器
|
|
|
+- 实现 `waitForOptionAndSelect` 函数处理异步等待
|
|
|
+- 使用重试机制而非固定超时
|
|
|
+- 超时时获取可用选项用于错误提示
|
|
|
+- 遵循 Story 1.3 的代码审查经验(`allTextContents`、`:text-is()`、完整 JSDoc)
|
|
|
+
|
|
|
+### File List
|
|
|
+
|
|
|
+**本故事需要创建/修改的文件:**
|
|
|
+- `packages/e2e-test-utils/src/radix-select.ts` - 添加异步 Select 工具函数(修改)
|
|
|
+- `packages/e2e-test-utils/src/index.ts` - 更新导出(修改)
|
|
|
+
|
|
|
+**相关文件(已在 Story 1.1、1.2、1.3 中完成,本故事使用):**
|
|
|
+- `packages/e2e-test-utils/src/types.ts` - 共享类型定义(AsyncSelectOptions 已存在)
|
|
|
+- `packages/e2e-test-utils/src/errors.ts` - 错误类和错误处理
|
|
|
+- `packages/e2e-test-utils/src/constants.ts` - 超时和选择器策略常量(DEFAULT_TIMEOUTS.async 已存在)
|
|
|
+
|
|
|
+**只读参考文件:**
|
|
|
+- `_bmad-output/implementation-artifacts/1-3-static-select-tool.md` - 前一个故事(静态 Select 实现)
|
|
|
+- `_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 测试标准
|
|
|
+
|