|
|
@@ -0,0 +1,607 @@
|
|
|
+# Story 2.2: 使用 selectRadixOption 重写残疾类型选择
|
|
|
+
|
|
|
+Status: ready-for-dev
|
|
|
+
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
+
|
|
|
+## Story
|
|
|
+
|
|
|
+作为测试开发者,
|
|
|
+我想要使用 `selectRadixOption()` 替换 Page Object 中的 Select 操作,
|
|
|
+以便验证工具在静态 Select 场景中的可用性。
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+
|
|
|
+1. **Given** @d8d/e2e-test-utils 已安装(Story 2.1 完成)
|
|
|
+2. **When** 修改 `web/tests/e2e/pages/admin/disability-person.page.ts`
|
|
|
+3. **Then** `fillBasicForm()` 中的残疾类型选择使用 `selectRadixOption()`
|
|
|
+4. **And** `fillBasicForm()` 中的残疾等级选择使用 `selectRadixOption()`
|
|
|
+5. **And** `addBankCard()` 中的银行卡名称选择使用 `selectRadixOption()`
|
|
|
+6. **And** `addBankCard()` 中的银行卡类型选择使用 `selectRadixOption()`(如适用)
|
|
|
+7. **And** `addVisit()` 中的回访类型选择使用 `selectRadixOption()`
|
|
|
+8. **And** 移除原有的 `selectRadixOption()` 方法(第 96-108 行)
|
|
|
+9. **And** 测试通过,功能正常
|
|
|
+
|
|
|
+## Tasks / Subtasks
|
|
|
+
|
|
|
+- [ ] 导入 selectRadixOption 工具函数 (AC: #1)
|
|
|
+ - [ ] 在文件顶部添加 `import { selectRadixOption } from '@d8d/e2e-test-utils'`
|
|
|
+- [ ] 替换 fillBasicForm 中的静态 Select 调用 (AC: #3, #4)
|
|
|
+ - [ ] 替换残疾类型选择:`await selectRadixOption(this.page, '残疾类型 *', data.disabilityType)`
|
|
|
+ - [ ] 替换残疾等级选择:`await selectRadixOption(this.page, '残疾等级 *', data.disabilityLevel)`
|
|
|
+- [ ] 替换 addBankCard 中的静态 Select 调用 (AC: #5, #6)
|
|
|
+ - [ ] 替换银行名称选择:`await selectRadixOption(this.page, '银行名称', bankCard.bankName)`
|
|
|
+ - [ ] 替换银行卡类型选择(如适用):`await selectRadixOption(this.page, '银行卡类型', bankCard.cardType)`
|
|
|
+- [ ] 替换 addVisit 中的静态 Select 调用 (AC: #7)
|
|
|
+ - [ ] 替换回访类型选择:`await selectRadixOption(this.page, '回访类型', visit.visitType)`
|
|
|
+- [ ] 移除原有的 selectRadixOption 方法 (AC: #8)
|
|
|
+ - [ ] 删除第 96-108 行的自定义方法实现
|
|
|
+- [ ] 验证测试通过 (AC: #9)
|
|
|
+ - [ ] 运行 `pnpm test:e2e:chromium disability-person-complete.spec.ts`
|
|
|
+ - [ ] 确认所有 Select 操作正常工作
|
|
|
+
|
|
|
+## Dev Notes
|
|
|
+
|
|
|
+### Epic Context
|
|
|
+
|
|
|
+**Epic 2 目标:** 在 `web/tests/e2e/` 的现有残疾人管理测试中使用 Select 工具,验证工具在真实场景中的可用性和稳定性。
|
|
|
+
|
|
|
+**Epic 2 范围:**
|
|
|
+- ✅ 使用现有 `web/tests/e2e/` 测试基础设施
|
|
|
+- ✅ 使用现有的残疾人管理测试场景
|
|
|
+- ❌ 不创建新的测试应用
|
|
|
+- ❌ 不添加新功能(仅验证现有功能)
|
|
|
+
|
|
|
+### 验证场景
|
|
|
+
|
|
|
+**静态 Select 场景(本故事):**
|
|
|
+- 残疾类型:视力残疾、听力残疾、肢体残疾、言语残疾、智力残疾、精神残疾
|
|
|
+- 残疾等级:一级、二级、三级、四级
|
|
|
+- 银行名称(addBankCard 方法)
|
|
|
+- 银行卡类型(addBankCard 方法,可选)
|
|
|
+- 回访类型(addVisit 方法)
|
|
|
+
|
|
|
+**异步 Select 场景(Story 2.3):**
|
|
|
+- 省份选择
|
|
|
+- 城市选择(根据省份动态加载)
|
|
|
+
|
|
|
+### 实现要点
|
|
|
+
|
|
|
+#### 1. 导入工具函数
|
|
|
+
|
|
|
+在文件顶部添加导入:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// web/tests/e2e/pages/admin/disability-person.page.ts
|
|
|
+import { Page, Locator } from '@playwright/test';
|
|
|
+import { selectRadixOption } from '@d8d/e2e-test-utils'; // 新增
|
|
|
+```
|
|
|
+
|
|
|
+#### 2. 替换调用方式
|
|
|
+
|
|
|
+**原实现(自定义方法):**
|
|
|
+```typescript
|
|
|
+// this.selectRadixOption 是类方法
|
|
|
+await this.selectRadixOption('残疾类型 *', data.disabilityType);
|
|
|
+```
|
|
|
+
|
|
|
+**新实现(工具函数):**
|
|
|
+```typescript
|
|
|
+// selectRadixOption 是导入的工具函数,需要传入 page 对象
|
|
|
+await selectRadixOption(this.page, '残疾类型 *', data.disabilityType);
|
|
|
+```
|
|
|
+
|
|
|
+**关键差异:**
|
|
|
+- 工具函数需要显式传入 `page` 对象作为第一个参数
|
|
|
+- 函数签名:`selectRadixOption(page: Page, label: string, value: string): Promise<void>`
|
|
|
+
|
|
|
+#### 3. 需要替换的位置
|
|
|
+
|
|
|
+| 方法 | 行号 | Select 字段 | 标签文本 |
|
|
|
+|------|------|-------------|----------|
|
|
|
+| `fillBasicForm` | 85 | 残疾类型 | `残疾类型 *` |
|
|
|
+| `fillBasicForm` | 86 | 残疾等级 | `残疾等级 *` |
|
|
|
+| `addBankCard` | 239 | 银行名称 | `银行名称` |
|
|
|
+| `addBankCard` | 246 | 银行卡类型 | `银行卡类型` |
|
|
|
+| `addVisit` | 310 | 回访类型 | `回访类型` |
|
|
|
+
|
|
|
+#### 4. 移除自定义方法
|
|
|
+
|
|
|
+删除第 96-108 行的自定义 `selectRadixOption` 方法:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 删除以下代码(第 96-108 行)
|
|
|
+async selectRadixOption(label: string, value: string) {
|
|
|
+ const combobox = this.page.getByRole('combobox', { name: label });
|
|
|
+ await combobox.click({ timeout: 2000 });
|
|
|
+ const option = this.page.getByRole('option', { name: value }).first();
|
|
|
+ await option.click({ timeout: 3000 });
|
|
|
+ console.log(` ✓ ${label} 选中: ${value}`);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 5. 保留 console.log 输出
|
|
|
+
|
|
|
+工具函数内部没有 console.log,如果需要保留调试输出,可以在调用后添加:
|
|
|
+
|
|
|
+```typescript
|
|
|
+await selectRadixOption(this.page, '残疾类型 *', data.disabilityType);
|
|
|
+console.debug(` ✓ 残疾类型选中: ${data.disabilityType}`);
|
|
|
+```
|
|
|
+
|
|
|
+### 依赖关系
|
|
|
+
|
|
|
+**前置依赖:**
|
|
|
+- Epic 1: ✅ 已完成(Select 工具已开发)
|
|
|
+- Story 2.1: ✅ 已完成(@d8d/e2e-test-utils 已安装)
|
|
|
+
|
|
|
+**后续故事:**
|
|
|
+- Story 2.3: 使用 selectRadixOptionAsync 重写省份/城市选择
|
|
|
+- Story 2.4: 运行测试并收集问题和改进建议
|
|
|
+
|
|
|
+### 验证步骤
|
|
|
+
|
|
|
+1. **代码修改完成后:**
|
|
|
+ ```bash
|
|
|
+ # 类型检查
|
|
|
+ pnpm typecheck
|
|
|
+ ```
|
|
|
+
|
|
|
+2. **运行测试:**
|
|
|
+ ```bash
|
|
|
+ # 运行残疾人管理完整流程测试
|
|
|
+ pnpm test:e2e:chromium disability-person-complete.spec.ts
|
|
|
+ ```
|
|
|
+
|
|
|
+3. **观察点:**
|
|
|
+ - Select 操作是否正常完成
|
|
|
+ - 是否有任何错误消息
|
|
|
+ - 测试是否通过
|
|
|
+
|
|
|
+### 错误处理
|
|
|
+
|
|
|
+如果工具函数抛出 `E2ETestError`:
|
|
|
+
|
|
|
+1. **触发器未找到:**
|
|
|
+ ```
|
|
|
+ Error: Radix Select 触发器未找到
|
|
|
+ 标签: 残疾类型 *
|
|
|
+ 期望值: 视力残疾
|
|
|
+ 建议: 检查下拉框标签是否正确,或添加 data-testid 属性
|
|
|
+ ```
|
|
|
+ - 检查标签文本是否正确(注意空格和星号)
|
|
|
+ - 确认元素是否在页面上可见
|
|
|
+
|
|
|
+2. **选项未找到:**
|
|
|
+ ```
|
|
|
+ Error: Radix Select 选项 "xxx" 未找到
|
|
|
+ 标签: 残疾类型
|
|
|
+ 期望值: xxx
|
|
|
+ 可用选项: 视力残疾, 听力残疾, 肢体残疾, ...
|
|
|
+ 建议: 检查选项值是否正确,或确认选项已加载到 DOM 中
|
|
|
+ ```
|
|
|
+ - 检查选项值是否与页面显示完全一致
|
|
|
+ - 确认选项是否已加载到 DOM 中
|
|
|
+
|
|
|
+### Project Structure Notes
|
|
|
+
|
|
|
+**文件路径:**
|
|
|
+```
|
|
|
+web/tests/e2e/pages/admin/disability-person.page.ts
|
|
|
+```
|
|
|
+
|
|
|
+**相关测试文件:**
|
|
|
+```
|
|
|
+web/tests/e2e/specs/admin/disability-person-complete.spec.ts
|
|
|
+```
|
|
|
+
|
|
|
+**工具包位置:**
|
|
|
+```
|
|
|
+packages/e2e-test-utils/
|
|
|
+```
|
|
|
+
|
|
|
+### References
|
|
|
+
|
|
|
+**Epic 2 详情:** `_bmad-output/planning-artifacts/epics.md#Epic-2`
|
|
|
+
|
|
|
+**Story 2.1(安装):** `_bmad-output/implementation-artifacts/2-1-install-e2e-utils.md`
|
|
|
+
|
|
|
+**Epic 1 回顾(技术经验):** `_bmad-output/implementation-artifacts/epic-1-retrospective.md`
|
|
|
+
|
|
|
+**工具包源码:** `packages/e2e-test-utils/src/radix-select.ts`
|
|
|
+
|
|
|
+**项目上下文:** `_bmad-output/project-context.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Developer Context
|
|
|
+
|
|
|
+> **重要提示:** 本部分包含开发者实现此故事所需的所有关键上下文和约束条件。
|
|
|
+
|
|
|
+### 技术需求
|
|
|
+
|
|
|
+#### 1. 工具函数签名
|
|
|
+
|
|
|
+`selectRadixOption` 函数来自 `@d8d/e2e-test-utils` 包:
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { selectRadixOption } from '@d8d/e2e-test-utils';
|
|
|
+
|
|
|
+// 函数签名
|
|
|
+selectRadixOption(page: Page, label: string, value: string): Promise<void>
|
|
|
+```
|
|
|
+
|
|
|
+**参数说明:**
|
|
|
+- `page`: Playwright Page 对象
|
|
|
+- `label`: 下拉框的标签文本(用于定位触发器)
|
|
|
+- `value`: 要选择的选项值
|
|
|
+
|
|
|
+**返回值:** Promise<void>
|
|
|
+
|
|
|
+**异常:** `E2ETestError` - 当触发器或选项未找到时
|
|
|
+
|
|
|
+#### 2. 选择器策略
|
|
|
+
|
|
|
+工具函数按以下优先级查找触发器:
|
|
|
+
|
|
|
+1. **data-testid(推荐):** `[data-testid="${label}-trigger"]`
|
|
|
+2. **aria-label + role:** `[aria-label="${label}"][role="combobox"]`
|
|
|
+3. **文本匹配(兜底):** `text="${label}"`
|
|
|
+
|
|
|
+**选项选择策略:**
|
|
|
+1. **data-value(精确匹配):** `[role="option"][data-value="${value}"]`
|
|
|
+2. **精确文本匹配:** `[role="option"]:text-is("${value}")`
|
|
|
+
|
|
|
+#### 3. 当前实现分析
|
|
|
+
|
|
|
+**文件位置:** `web/tests/e2e/pages/admin/disability-person.page.ts`
|
|
|
+
|
|
|
+**现有代码(需要修改的部分):**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 第 1-2 行:导入部分
|
|
|
+import { Page, Locator } from '@playwright/test';
|
|
|
+// 需要添加:import { selectRadixOption } from '@d8d/e2e-test-utils';
|
|
|
+
|
|
|
+// 第 64-94 行:fillBasicForm 方法
|
|
|
+async fillBasicForm(data: {
|
|
|
+ // ... 数据字段
|
|
|
+}) {
|
|
|
+ await this.page.waitForSelector('form#create-form', { state: 'visible', timeout: 5000 });
|
|
|
+ await this.page.getByLabel('姓名 *').fill(data.name);
|
|
|
+ await this.page.getByLabel('性别 *').selectOption(data.gender); // 原生 select,不需要改
|
|
|
+ await this.page.getByLabel('身份证号 *').fill(data.idCard);
|
|
|
+ await this.page.getByLabel('残疾证号 *').fill(data.disabilityId);
|
|
|
+ await this.selectRadixOption('残疾类型 *', data.disabilityType); // ❌ 需要替换
|
|
|
+ await this.selectRadixOption('残疾等级 *', data.disabilityLevel); // ❌ 需要替换
|
|
|
+ // ... 其他代码
|
|
|
+ await this.selectRadixOption('省份 *', data.province); // Story 2.3 处理
|
|
|
+ await this.page.waitForTimeout(500); // Story 2.3 会移除这个 hack
|
|
|
+ await this.selectRadixOption('城市', data.city); // Story 2.3 处理
|
|
|
+}
|
|
|
+
|
|
|
+// 第 96-108 行:自定义方法(需要删除)
|
|
|
+async selectRadixOption(label: string, value: string) {
|
|
|
+ const combobox = this.page.getByRole('combobox', { name: label });
|
|
|
+ await combobox.click({ timeout: 2000 });
|
|
|
+ const option = this.page.getByRole('option', { name: value }).first();
|
|
|
+ await option.click({ timeout: 3000 });
|
|
|
+ console.log(` ✓ ${label} 选中: ${value}`);
|
|
|
+}
|
|
|
+
|
|
|
+// 第 225-261 行:addBankCard 方法
|
|
|
+async addBankCard(bankCard: {
|
|
|
+ bankName: string;
|
|
|
+ subBankName: string;
|
|
|
+ cardNumber: string;
|
|
|
+ cardholderName: string;
|
|
|
+ cardType?: string;
|
|
|
+ photoFileName?: string;
|
|
|
+}) {
|
|
|
+ await this.page.getByRole('button', { name: /添加银行卡/ }).click();
|
|
|
+ await this.page.waitForTimeout(300);
|
|
|
+ await this.selectRadixOption('银行名称', bankCard.bankName); // ❌ 需要替换
|
|
|
+ await this.page.getByLabel(/发卡支行/).fill(bankCard.subBankName);
|
|
|
+ await this.page.getByLabel(/银行卡号/).fill(bankCard.cardNumber);
|
|
|
+ await this.page.getByLabel(/持卡人姓名/).fill(bankCard.cardholderName);
|
|
|
+ if (bankCard.cardType) {
|
|
|
+ await this.selectRadixOption('银行卡类型', bankCard.cardType); // ❌ 需要替换
|
|
|
+ }
|
|
|
+ // ... 其他代码
|
|
|
+}
|
|
|
+
|
|
|
+// 第 296-332 行:addVisit 方法
|
|
|
+async addVisit(visit: {
|
|
|
+ visitDate: string;
|
|
|
+ visitType: string;
|
|
|
+ visitContent: string;
|
|
|
+ visitResult?: string;
|
|
|
+ nextVisitDate?: string;
|
|
|
+}) {
|
|
|
+ await this.page.getByRole('button', { name: /添加回访/ }).click();
|
|
|
+ await this.page.waitForTimeout(300);
|
|
|
+ await this.page.getByLabel(/回访日期/).fill(visit.visitDate);
|
|
|
+ await this.selectRadixOption('回访类型', visit.visitType); // ❌ 需要替换
|
|
|
+ // ... 其他代码
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 4. 修改清单
|
|
|
+
|
|
|
+| 步骤 | 操作 | 位置 | 详情 |
|
|
|
+|------|------|------|------|
|
|
|
+| 1 | 添加导入 | 第 3 行后 | `import { selectRadixOption } from '@d8d/e2e-test-utils';` |
|
|
|
+| 2 | 替换调用 | 第 85 行 | `await selectRadixOption(this.page, '残疾类型 *', data.disabilityType)` |
|
|
|
+| 3 | 替换调用 | 第 86 行 | `await selectRadixOption(this.page, '残疾等级 *', data.disabilityLevel)` |
|
|
|
+| 4 | 替换调用 | 第 239 行 | `await selectRadixOption(this.page, '银行名称', bankCard.bankName)` |
|
|
|
+| 5 | 替换调用 | 第 246 行 | `await selectRadixOption(this.page, '银行卡类型', bankCard.cardType)` |
|
|
|
+| 6 | 替换调用 | 第 310 行 | `await selectRadixOption(this.page, '回访类型', visit.visitType)` |
|
|
|
+| 7 | 删除方法 | 第 96-108 行 | 删除整个 `selectRadixOption` 方法 |
|
|
|
+
|
|
|
+#### 5. 完整修改示例
|
|
|
+
|
|
|
+**修改前:**
|
|
|
+```typescript
|
|
|
+async fillBasicForm(data: { /* ... */ }) {
|
|
|
+ // ...
|
|
|
+ await this.selectRadixOption('残疾类型 *', data.disabilityType);
|
|
|
+ await this.selectRadixOption('残疾等级 *', data.disabilityLevel);
|
|
|
+ // ...
|
|
|
+}
|
|
|
+
|
|
|
+async selectRadixOption(label: string, value: string) {
|
|
|
+ const combobox = this.page.getByRole('combobox', { name: label });
|
|
|
+ await combobox.click({ timeout: 2000 });
|
|
|
+ const option = this.page.getByRole('option', { name: value }).first();
|
|
|
+ await option.click({ timeout: 3000 });
|
|
|
+ console.log(` ✓ ${label} 选中: ${value}`);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**修改后:**
|
|
|
+```typescript
|
|
|
+import { selectRadixOption } from '@d8d/e2e-test-utils'; // 新增
|
|
|
+
|
|
|
+async fillBasicForm(data: { /* ... */ }) {
|
|
|
+ // ...
|
|
|
+ await selectRadixOption(this.page, '残疾类型 *', data.disabilityType); // 修改
|
|
|
+ await selectRadixOption(this.page, '残疾等级 *', data.disabilityLevel); // 修改
|
|
|
+ // ...
|
|
|
+}
|
|
|
+
|
|
|
+// 删除 selectRadixOption 方法
|
|
|
+```
|
|
|
+
|
|
|
+### 架构合规性
|
|
|
+
|
|
|
+#### 工具包设计原则
|
|
|
+
|
|
|
+**单一职责:**
|
|
|
+- `selectRadixOption` 专注于静态 Select 选择
|
|
|
+- 不处理异步加载场景(使用 `selectRadixOptionAsync`)
|
|
|
+
|
|
|
+**错误处理:**
|
|
|
+- 使用 `E2ETestError` 提供结构化错误信息
|
|
|
+- 错误包含:操作类型、目标、期望值、可用选项、修复建议
|
|
|
+
|
|
|
+**超时配置:**
|
|
|
+- 静态 Select 默认超时:2000ms(`DEFAULT_TIMEOUTS.static`)
|
|
|
+- 使用 Playwright 的 auto-waiting 机制
|
|
|
+
|
|
|
+#### TypeScript 严格模式
|
|
|
+
|
|
|
+**工具包类型定义:**
|
|
|
+```typescript
|
|
|
+// packages/e2e-test-utils/src/radix-select.ts
|
|
|
+export async function selectRadixOption(
|
|
|
+ page: Page,
|
|
|
+ label: string,
|
|
|
+ value: string
|
|
|
+): Promise<void>
|
|
|
+```
|
|
|
+
|
|
|
+**使用时类型检查:**
|
|
|
+- IDE 自动补全函数参数
|
|
|
+- 编译时检查参数类型
|
|
|
+- 错误时抛出 `E2ETestError`
|
|
|
+
|
|
|
+### 库和框架要求
|
|
|
+
|
|
|
+#### Playwright 版本
|
|
|
+
|
|
|
+| 包 | 版本 | 要求 |
|
|
|
+|---|------|------|
|
|
|
+| @playwright/test (web) | 1.55.0 | ✅ 满足 ^1.40.0 |
|
|
|
+| @d8d/e2e-test-utils | - | peer dependency: ^1.40.0 |
|
|
|
+
|
|
|
+#### 工具包 API
|
|
|
+
|
|
|
+**导出的函数:**
|
|
|
+```typescript
|
|
|
+// packages/e2e-test-utils/src/index.ts
|
|
|
+export { selectRadixOption } from './radix-select.js';
|
|
|
+export { selectRadixOptionAsync } from './radix-select.js';
|
|
|
+
|
|
|
+// 类型
|
|
|
+export type { BaseOptions } from './types.js';
|
|
|
+export type { AsyncSelectOptions } from './radix-select.js';
|
|
|
+
|
|
|
+// 错误
|
|
|
+export { E2ETestError } from './errors.js';
|
|
|
+export type { ErrorContext } from './errors.js';
|
|
|
+```
|
|
|
+
|
|
|
+### 文件结构要求
|
|
|
+
|
|
|
+#### 修改文件清单
|
|
|
+
|
|
|
+**主要修改:**
|
|
|
+- `web/tests/e2e/pages/admin/disability-person.page.ts`
|
|
|
+
|
|
|
+**不影响其他文件:**
|
|
|
+- `web/tests/e2e/specs/admin/disability-person-complete.spec.ts`(测试文件不需要修改)
|
|
|
+- `web/package.json`(依赖已在 Story 2.1 添加)
|
|
|
+
|
|
|
+#### 工具包位置
|
|
|
+
|
|
|
+```
|
|
|
+packages/e2e-test-utils/
|
|
|
+├── src/
|
|
|
+│ ├── radix-select.ts # selectRadixOption 实现
|
|
|
+│ ├── types.ts # 类型定义
|
|
|
+│ ├── errors.ts # 错误处理
|
|
|
+│ ├── constants.ts # 常量(超时配置)
|
|
|
+│ └── index.ts # 主导出
|
|
|
+├── package.json
|
|
|
+└── README.md
|
|
|
+```
|
|
|
+
|
|
|
+### 测试要求
|
|
|
+
|
|
|
+#### 验证步骤
|
|
|
+
|
|
|
+**1. 代码修改完成后:**
|
|
|
+```bash
|
|
|
+# 类型检查
|
|
|
+pnpm typecheck
|
|
|
+```
|
|
|
+
|
|
|
+**2. 运行测试:**
|
|
|
+```bash
|
|
|
+# 运行残疾人管理完整流程测试
|
|
|
+pnpm test:e2e:chromium disability-person-complete.spec.ts
|
|
|
+```
|
|
|
+
|
|
|
+**3. 观察点:**
|
|
|
+- ✅ 所有 Select 操作正常完成
|
|
|
+- ✅ 无 `E2ETestError` 错误
|
|
|
+- ✅ 测试通过
|
|
|
+- ✅ 控制台输出正常(如有添加 console.debug)
|
|
|
+
|
|
|
+#### 预期测试行为
|
|
|
+
|
|
|
+**测试场景:**
|
|
|
+```typescript
|
|
|
+// 测试代码会调用 fillBasicForm
|
|
|
+await page.fillBasicForm({
|
|
|
+ name: '测试用户',
|
|
|
+ gender: '男',
|
|
|
+ idCard: '110101199001011234',
|
|
|
+ disabilityId: '1101011990',
|
|
|
+ disabilityType: '视力残疾', // 使用 selectRadixOption
|
|
|
+ disabilityLevel: '一级', // 使用 selectRadixOption
|
|
|
+ phone: '13800138000',
|
|
|
+ idAddress: '北京市东城区',
|
|
|
+ province: '广东省', // Story 2.3 处理
|
|
|
+ city: '深圳市', // Story 2.3 处理
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+**预期结果:**
|
|
|
+- 残疾类型下拉框展开并选中"视力残疾"
|
|
|
+- 残疾等级下拉框展开并选中"一级"
|
|
|
+- 测试继续执行,无错误
|
|
|
+
|
|
|
+### 上一个故事的经验(Epic 1 Retrospective)
|
|
|
+
|
|
|
+#### 关键经验总结
|
|
|
+
|
|
|
+**1. TypeScript + Playwright 陷阱 [来源: epic-1-retrospective.md]**
|
|
|
+- ❌ 避免使用 `page.evaluate()` 获取文本
|
|
|
+- ✅ 使用 Playwright API:`page.locator().allTextContents()`
|
|
|
+
|
|
|
+**2. 精确文本匹配 [来源: epic-1-retrospective.md]**
|
|
|
+- ❌ 使用 `:has-text()` 会部分匹配,可能误选
|
|
|
+- ✅ 使用 `:text-is()` 进行精确匹配
|
|
|
+- 工具函数已内置此修复,无需担心
|
|
|
+
|
|
|
+**3. 代码审查发现的问题类型:**
|
|
|
+- HIGH: DOM 类型问题、精确文本匹配
|
|
|
+- MEDIUM: 错误消息不清晰
|
|
|
+- LOW: 代码风格
|
|
|
+
|
|
|
+**4. 测试覆盖率目标:**
|
|
|
+- ≥80% 覆盖率(Epic 1 已达 93.65%)
|
|
|
+
|
|
|
+#### 需要注意的技术决策
|
|
|
+
|
|
|
+**选择器策略优先级:**
|
|
|
+1. `data-testid` - 最高优先级(推荐在 Radix 组件上添加)
|
|
|
+2. `aria-label` + role - 无障碍标准
|
|
|
+3. Text content + role - 兜底方案
|
|
|
+
|
|
|
+**错误处理模式:**
|
|
|
+- 使用 `E2ETestError` 而非原生 `Error`
|
|
|
+- 提供结构化的错误上下文(标签、期望值、可用选项)
|
|
|
+
|
|
|
+### Git Intelligence
|
|
|
+
|
|
|
+**最近 5 次提交:**
|
|
|
+```
|
|
|
+02ece3b fix(e2e-test-utils): 完成 Story 2.1 代码审查修复
|
|
|
+35bde40 docs(e2e-test-utils): 创建 Story 2.1 安装 e2e-utils 包
|
|
|
+54bded5 docs: 完成 Epic 1 回顾及技术改进
|
|
|
+f72ea78 test(e2e-test-utils): 完成 Story 1.6 Select 工具单元测试及代码审查
|
|
|
+5350962 ✨ feat(rpc-client): 添加辅助函数以不区分大小写获取响应头
|
|
|
+```
|
|
|
+
|
|
|
+**相关文件修改历史:**
|
|
|
+- `web/tests/e2e/pages/admin/disability-person.page.ts` - 在 Story 2.1 中添加了注释说明工具包已安装
|
|
|
+
|
|
|
+**代码模式:**
|
|
|
+- 提交信息使用中文
|
|
|
+- 使用 conventional commits 格式(feat, fix, docs 等)
|
|
|
+
|
|
|
+### 项目上下文引用
|
|
|
+
|
|
|
+**完整项目上下文:** `_bmad-output/project-context.md`
|
|
|
+
|
|
|
+**关键规范:**
|
|
|
+- 测试框架:Playwright 1.55.0
|
|
|
+- 包管理:pnpm workspace 协议
|
|
|
+- TypeScript:严格模式,无 `any` 类型
|
|
|
+- 测试命令:`pnpm test:e2e:chromium`
|
|
|
+
|
|
|
+**相关文档:**
|
|
|
+- Epic 2 详情:`_bmad-output/planning-artifacts/epics.md#Epic-2`
|
|
|
+- Story 2.1:`_bmad-output/implementation-artifacts/2-1-install-e2e-utils.md`
|
|
|
+- Epic 1 回顾:`_bmad-output/implementation-artifacts/epic-1-retrospective.md`
|
|
|
+
|
|
|
+### 下一步
|
|
|
+
|
|
|
+完成本故事后,继续:
|
|
|
+- Story 2.3: 使用 selectRadixOptionAsync 重写省份/城市选择
|
|
|
+- Story 2.4: 运行测试并收集问题和改进建议
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Dev Agent Record
|
|
|
+
|
|
|
+### Agent Model Used
|
|
|
+
|
|
|
+Claude Opus 4.5 (claude-opus-4-5-20251101)
|
|
|
+
|
|
|
+### Debug Log References
|
|
|
+
|
|
|
+(开发过程中添加调试日志引用)
|
|
|
+
|
|
|
+### Completion Notes List
|
|
|
+
|
|
|
+(实现完成后添加完成备注)
|
|
|
+
|
|
|
+### File List
|
|
|
+
|
|
|
+(实现完成后添加修改文件列表)
|
|
|
+
|
|
|
+### Change Log
|
|
|
+
|
|
|
+**创建时间:** 2026-01-09
|
|
|
+
|
|
|
+**创建内容:**
|
|
|
+- 完整的 Story 2.2 文档,包含:
|
|
|
+ - 用户故事和验收标准
|
|
|
+ - 详细的任务分解
|
|
|
+ - 开发者上下文(技术需求、架构合规性、测试要求)
|
|
|
+ - Epic 1 回顾经验总结
|
|
|
+ - Git 情报和项目上下文引用
|
|
|
+
|