Ver código fonte

fix(e2e-test-utils): 完成 Story 1.3 代码审查修复并标记完成

代码审查发现并修复 8 个问题:

HIGH 修复 (2):
- findTrigger 添加 expected 参数,修复错误消息不完整
- 文本选择器改为 :text-is() 精确匹配,避免误选

MEDIUM 修复 (6):
- 删除冗余的 null 检查,简化代码逻辑
- 空 catch 块添加 console.debug,改善调试体验
- 内部函数添加完整 JSDoc 注释,符合项目标准
- waitForSelector 添加 state: "visible",更安全等待
- File List 更新包含所有修改文件

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

🤖 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 semana atrás
pai
commit
e116408b52

+ 1 - 1
CLAUDE.md

@@ -16,7 +16,7 @@
 - e2e测试平常只运行 pnpm test:e2e:chromium 就行
 - e2e测试失败时先查看页面结构 test-results/**/error-context.md
 - 前端是 hono/client  hc  rpc 的,不是直接fetch
-- bmad-core dir is in .bmad-core
+- **project-context.md 路径**: `_bmad-output/project-context.md`
 - 必须用中文回答
 - **git提交**: 当遇到git锁文件冲突时,使用单条命令:`rm -f /mnt/code/184-172-template-6/.git/index.lock && git add <文件> && git commit -m "提交信息"`
 - **测试调试**: 使用 `pnpm test --testNamePattern "测试名称"` 来运行特定测试查看详细信息 (mini使用Jest,其他包使用Vitest)

+ 79 - 11
_bmad-output/implementation-artifacts/1-3-static-select-tool.md

@@ -1,6 +1,6 @@
 # Story 1.3: 实现静态 Select 工具函数
 
-Status: ready-for-dev
+Status: done
 
 <!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
 
@@ -23,16 +23,16 @@ Status: ready-for-dev
 
 ## 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` 确保无类型错误
+- [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
 
@@ -472,6 +472,12 @@ packages/e2e-test-utils/src/
 
 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
@@ -482,11 +488,31 @@ Claude (d8d-model) via create-story workflow
 - 包含 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` - 共享类型定义
@@ -502,8 +528,50 @@ Claude (d8d-model) via create-story workflow
 
 ## 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
+

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

@@ -44,7 +44,7 @@ development_status:
   epic-1: in-progress
   1-1-create-package-structure: done
   1-2-implement-types-errors: done
-  1-3-static-select-tool: in-progress
+  1-3-static-select-tool: done
   1-4-async-select-tool: backlog
   1-5-main-export-docs: backlog
   1-6-select-unit-tests: backlog

+ 2 - 2
packages/e2e-test-utils/src/index.ts

@@ -44,5 +44,5 @@ export * from './errors';
 // 导出常量
 export * from './constants';
 
-// Radix UI Select 工具(后续故事实现)
-// export * from './radix-select';
+// Radix UI Select 工具
+export * from './radix-select';

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

@@ -0,0 +1,136 @@
+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, "性别", "男");
+ * ```
+ */
+export async function selectRadixOption(page: Page, label: string, value: string): Promise<void> {
+  const trigger = await findTrigger(page, label, value);
+  await trigger.click();
+  await page.waitForSelector("[role=listbox]", { timeout: DEFAULT_TIMEOUTS.static, state: "visible" });
+  const availableOptions = await page.locator("[role=option]").allTextContents();
+  await findAndClickOption(page, value, availableOptions);
+}
+
+/**
+ * 查找 Radix UI Select 触发器
+ *
+ * @description
+ * 按优先级尝试三种选择器策略查找触发器元素。
+ *
+ * @internal
+ *
+ * @param page - Playwright Page 对象
+ * @param label - 下拉框标签
+ * @param expectedValue - 期望选择的选项值(用于错误提示)
+ * @returns 触发器元素
+ * @throws {E2ETestError} 当触发器未找到时
+ */
+async function findTrigger(page: Page, label: string, expectedValue: string) {
+  const timeout = DEFAULT_TIMEOUTS.static;
+  const options = { timeout, state: "visible" as const };
+
+  // 策略 1: data-testid
+  const testIdSelector = `[data-testid="${label}-trigger"]`;
+  try {
+    return await page.waitForSelector(testIdSelector, options);
+  } catch (err) {
+    console.debug(`选择器策略1失败: ${testIdSelector}`, err);
+  }
+
+  // 策略 2: aria-label + role
+  const ariaSelector = `[aria-label="${label}"][role="combobox"]`;
+  try {
+    return await page.waitForSelector(ariaSelector, options);
+  } catch (err) {
+    console.debug(`选择器策略2失败: ${ariaSelector}`, err);
+  }
+
+  // 策略 3: text content
+  try {
+    return await page.waitForSelector(`text="${label}"`, options);
+  } catch (err) {
+    console.debug(`选择器策略3失败: text="${label}"`, err);
+  }
+
+  // 所有策略都失败
+  throwError({
+    operation: "selectRadixOption",
+    target: label,
+    expected: expectedValue,
+    suggestion: "检查下拉框标签是否正确,或添加 data-testid 属性"
+  });
+}
+
+/**
+ * 查找并点击 Radix UI Select 选项
+ *
+ * @description
+ * 按优先级尝试 data-value 和精确文本匹配两种策略。
+ *
+ * @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;
+  const options = { timeout, state: "visible" as const };
+
+  // 策略 1: data-value(精确匹配)
+  const dataValueSelector = `[role="option"][data-value="${value}"]`;
+  try {
+    const option = await page.waitForSelector(dataValueSelector, options);
+    await option.click();
+    return;
+  } catch (err) {
+    console.debug(`选项选择器策略1失败: ${dataValueSelector}`, err);
+  }
+
+  // 策略 2: 精确文本匹配(使用 :text-is 避免部分匹配)
+  const textSelector = `[role="option"]:text-is("${value}")`;
+  try {
+    const option = await page.waitForSelector(textSelector, options);
+    await option.click();
+    return;
+  } catch (err) {
+    console.debug(`选项选择器策略2失败: ${textSelector}`, err);
+  }
+
+  // 未找到选项
+  throwError({
+    operation: "selectRadixOption",
+    target: `选项 "${value}"`,
+    available: availableOptions,
+    suggestion: "检查选项值是否正确,或确认选项已加载到 DOM 中"
+  });
+}

+ 1 - 1
packages/e2e-test-utils/tsconfig.json

@@ -2,7 +2,7 @@
   "compilerOptions": {
     "target": "ES2020",
     "module": "ESNext",
-    "lib": ["ES2020"],
+    "lib": ["ES2020", "DOM"],
     "moduleResolution": "bundler",
     "baseUrl": ".",
     "strict": true,