# Story 9.2: 银行卡管理功能测试

Status: done

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

## Story

作为测试开发者，
我想要编写银行卡管理功能的测试，
以便验证银行卡的添加、编辑、删除功能。

## Acceptance Criteria

**Given** 残疾人管理 Page Object 已存在
**When** 编写银行卡管理测试
**Then** 包含以下测试场景：

1. **添加银行卡**
   - 添加单张银行卡
   - 验证银行卡信息保存正确
   - 验证银行卡列表显示

2. **编辑银行卡**
   - 修改银行卡信息
   - 验证修改后数据更新

3. **删除银行卡**
   - 删除单张银行卡
   - 验证删除后列表更新

4. **多张银行卡管理**
   - 添加多张银行卡
   - 验证列表顺序
   - 验证默认银行卡设置

## Tasks / Subtasks

- [x] **Task 1: 分析银行卡管理功能的 DOM 结构** (AC: #1, #2, #3, #4)
  - [x] Subtask 1.1: 在残疾人管理页面中定位银行卡管理区域
  - [x] Subtask 1.2: 分析添加银行卡按钮和表单结构
  - [x] Subtask 1.3: 分析银行卡列表展示结构
  - [x] Subtask 1.4: 分析编辑和删除按钮的选择器

- [x] **Task 2: 创建银行卡测试文件** (AC: #1, #2, #3, #4)
  - [x] Subtask 2.1: 创建 `web/tests/e2e/specs/admin/disability-person-bankcard.spec.ts`
  - [x] Subtask 2.2: 编写添加银行卡测试
  - [x] Subtask 2.3: 编写编辑银行卡测试
  - [x] Subtask 2.4: 编写删除银行卡测试
  - [x] Subtask 2.5: 编写多张银行卡管理测试

- [x] **Task 3: 更新 Page Object** (AC: #1, #2, #3, #4)
  - [x] Subtask 3.1: 添加银行卡管理相关方法到 `DisabilityPersonManagementPage`
  - [x] Subtask 3.2: 实现 `addBankCard()` 方法
  - [x] Subtask 3.3: 实现 `editBankCard()` 方法
  - [x] Subtask 3.4: 实现 `deleteBankCard()` 方法
  - [x] Subtask 3.5: 实现 `getBankCardList()` 方法用于验证

- [x] **Task 4: 运行测试并验证通过** (AC: #1, #2, #3, #4)
  - [x] Subtask 4.1: 使用 `pnpm test:e2e:chromium disability-person-bankcard` 运行测试
  - [x] Subtask 4.2: 修复发现的问题
  - [x] Subtask 4.3: 验证所有测试通过

## Dev Notes

### Epic 9 背景与目标

**Epic 9: 残疾人管理完整 E2E 测试覆盖（含并行隔离）**

为残疾人管理功能编写完整的、真正验证业务功能的 E2E 测试，并确保测试可以与未来的区域管理测试并行运行。

**Epic 9 的 Story 依赖关系：**
- Story 9.1：照片上传功能测试 ✅ Done
- **Story 9.2（本故事）**：银行卡管理功能测试
- Story 9.3：备注管理功能测试
- Story 9.4：回访记录管理测试
- Story 9.5：完整流程测试（CRUD）
- Story 9.6：测试隔离与并行执行验证
- Story 9.7：稳定性验证（10次连续运行）

### 业务功能分析

**银行卡管理功能概述：**

残疾人管理系统中，每个残疾人可以关联多张银行卡，用于管理残疾人的工资发放、补贴发放等信息。

**典型功能流程：**
1. 在残疾人详情页或编辑对话框中找到银行卡管理区域
2. 点击"添加银行卡"按钮打开添加表单
3. 填写银行卡信息（银行名称、卡号、持卡人等）
4. 保存后银行卡显示在列表中
5. 可以编辑或删除已添加的银行卡
6. 可以设置默认银行卡

### 技术规范

#### 现有 Page Object 结构

**当前 Page Object 位置：** `web/tests/e2e/pages/admin/disability-person.page.ts`

**从 Story 9.1 学到的模式：**
- 使用 `data-testid` 选择器最稳定
- 在对话框内操作时，使用 `form.getByLabel()` 限制范围
- 表单提交使用 `form.handleSubmit()` 并配合 `console.debug` 调试验证错误

#### 测试文件结构

```
web/tests/e2e/
├── specs/
│   └── admin/
│       └── disability-person-bankcard.spec.ts  # 本测试文件（需创建）
└── pages/
    └── admin/
        └── disability-person.page.ts  # Page Object（需扩展）
```

#### 测试用例设计

**测试文件模板：**

```typescript
// web/tests/e2e/specs/admin/disability-person-bankcard.spec.ts
import { test, expect } from '@playwright/test';
import { DisabilityPersonManagementPage } from '../../pages/admin/disability-person.page';

test.describe('残疾人管理 - 银行卡管理功能', () => {
  let pageObject: DisabilityPersonManagementPage;
  const TIMESTAMP = Date.now();
  const UNIQUE_ID = `test_bankcard_${TIMESTAMP}`;

  test.beforeEach(async ({ page }) => {
    pageObject = new DisabilityPersonManagementPage(page);
    await pageObject.goto();
    // 打开创建对话框或编辑已有残疾人的对话框
    await pageObject.openCreateDialog();
    // 先填写基本信息才能添加银行卡
    await pageObject.fillBasicInfo({
      name: UNIQUE_ID,
      idCard: `110101199001011234`, // 测试身份证号
    });
  });

  test('应该成功添加单张银行卡', async ({ page }) => {
    const bankCardData = {
      bankName: '中国工商银行',
      cardNumber: '6222021234567890123',
      cardHolder: UNIQUE_ID,
      isDefault: true,
    };

    await pageObject.addBankCard(bankCardData);

    // 验证银行卡出现在列表中
    const bankCardList = await pageObject.getBankCardList();
    expect(bankCardList).toHaveLength(1);
    expect(bankCardList[0]).toContain(bankCardData.bankName);
    expect(bankCardList[0]).toContain(bankCardData.cardNumber.slice(-4)); // 验证后4位
  });

  test('应该成功编辑银行卡信息', async ({ page }) => {
    // 先添加一张银行卡
    const originalData = {
      bankName: '中国工商银行',
      cardNumber: '6222021234567890123',
      cardHolder: UNIQUE_ID,
    };
    await pageObject.addBankCard(originalData);

    // 编辑银行卡
    const updatedData = {
      bankName: '中国建设银行',
      cardNumber: '6227001234567890123',
      cardHolder: UNIQUE_ID,
    };
    await pageObject.editBankCard(0, updatedData);

    // 验证更新后的信息
    const bankCardList = await pageObject.getBankCardList();
    expect(bankCardList[0]).toContain(updatedData.bankName);
  });

  test('应该成功删除银行卡', async ({ page }) => {
    // 先添加一张银行卡
    const bankCardData = {
      bankName: '中国工商银行',
      cardNumber: '6222021234567890123',
      cardHolder: UNIQUE_ID,
    };
    await pageObject.addBankCard(bankCardData);

    // 验证银行卡存在
    let bankCardList = await pageObject.getBankCardList();
    expect(bankCardList).toHaveLength(1);

    // 删除银行卡
    await pageObject.deleteBankCard(0);

    // 验证银行卡已被删除
    bankCardList = await pageObject.getBankCardList();
    expect(bankCardList).toHaveLength(0);
  });

  test('应该支持添加多张银行卡', async ({ page }) => {
    // 添加多张银行卡
    await pageObject.addBankCard({
      bankName: '中国工商银行',
      cardNumber: '6222021234567890123',
      cardHolder: UNIQUE_ID,
    });
    await pageObject.addBankCard({
      bankName: '中国建设银行',
      cardNumber: '6227001234567890123',
      cardHolder: UNIQUE_ID,
    });
    await pageObject.addBankCard({
      bankName: '中国农业银行',
      cardNumber: '6228481234567890123',
      cardHolder: UNIQUE_ID,
    });

    // 验证所有银行卡都显示
    const bankCardList = await pageObject.getBankCardList();
    expect(bankCardList).toHaveLength(3);

    // 验证列表顺序（后添加的在前面还是后面，取决于业务逻辑）
  });

  test('应该能够设置默认银行卡', async ({ page }) => {
    // 添加两张银行卡
    await pageObject.addBankCard({
      bankName: '中国工商银行',
      cardNumber: '6222021234567890123',
      cardHolder: UNIQUE_ID,
      isDefault: true,
    });
    await pageObject.addBankCard({
      bankName: '中国建设银行',
      cardNumber: '6227001234567890123',
      cardHolder: UNIQUE_ID,
      isDefault: false,
    });

    // 验证默认银行卡标记
    const defaultCard = await pageObject.getDefaultBankCard();
    expect(defaultCard).toContain('中国工商银行');
    expect(defaultCard).toContain('默认');
  });
});
```

### Page Object 方法设计

**需要在 `DisabilityPersonManagementPage` 中添加的方法：**

```typescript
/**
 * 添加银行卡
 * @param bankCardData 银行卡数据
 */
async addBankCard(bankCardData: {
  bankName: string;
  cardNumber: string;
  cardHolder: string;
  isDefault?: boolean;
}): Promise<void> {
  // 1. 点击"添加银行卡"按钮
  // 2. 等待银行卡表单对话框打开
  // 3. 填写银行卡信息
  // 4. 如果 isDefault 为 true，设置默认银行卡
  // 5. 点击保存按钮
  // 6. 等待对话框关闭
}

/**
 * 编辑银行卡
 * @param index 银行卡索引（第几张，从0开始）
 * @param bankCardData 更新的银行卡数据
 */
async editBankCard(
  index: number,
  bankCardData: {
    bankName?: string;
    cardNumber?: string;
    cardHolder?: string;
    isDefault?: boolean;
  }
): Promise<void> {
  // 1. 找到指定索引的银行卡的编辑按钮
  // 2. 点击编辑按钮
  // 3. 等待编辑对话框打开
  // 4. 更新银行卡信息
  // 5. 点击保存按钮
  // 6. 等待对话框关闭
}

/**
 * 删除银行卡
 * @param index 银行卡索引（第几张，从0开始）
 */
async deleteBankCard(index: number): Promise<void> {
  // 1. 找到指定索引的银行卡的删除按钮
  // 2. 点击删除按钮
  // 3. 等待确认对话框
  // 4. 点击确认按钮
  // 5. 等待删除完成
}

/**
 * 获取银行卡列表
 * @returns 银行卡信息数组
 */
async getBankCardList(): Promise<string[]> {
  // 1. 定位银行卡列表容器
  // 2. 获取所有银行卡项的文本内容
  // 3. 返回银行卡信息数组
  return []; // 实现时返回真实数据
}

/**
 * 获取默认银行卡信息
 * @returns 默认银行卡的文本内容
 */
async getDefaultBankCard(): Promise<string> {
  // 1. 找到带有"默认"标记的银行卡
  // 2. 返回其文本内容
  return ''; // 实现时返回真实数据
}
```

### 选择器策略

**参考 Story 9.1 的经验：**

1. **添加银行卡按钮：** 使用 `page.getByRole('button', { name: /添加.*银行卡/ })`
2. **银行卡表单：** 在对话框内使用 `form.getByLabel()` 限制范围
3. **银行卡列表项：** 使用 `data-testid` 或 `role="listitem"` 选择器
4. **编辑/删除按钮：** 每个银行卡项内，使用索引或文本定位

**示例：**
```typescript
// 点击编辑按钮（第1张银行卡的编辑按钮）
const bankCards = page.locator('[data-testid="bankcard-item"]');
await bankCards.nth(0).getByRole('button', { name: /编辑/ }).click();

// 或使用更具体的选择器
await page.locator('[data-testid="bankcard-item-0"] [data-testid="edit-button"]').click();
```

### 数据隔离策略

**参考 Story 9.1 的数据隔离模式：**

```typescript
test.beforeEach(async ({ page }) => {
  // 使用时间戳确保数据唯一
  const timestamp = Date.now();
  const uniqueId = `test_bankcard_${timestamp}`;

  pageObject = new DisabilityPersonManagementPage(page);
  await pageObject.goto();
  await pageObject.openCreateDialog();
});

test.afterEach(async ({ page }) => {
  // 清理测试数据（根据实际业务逻辑实现）
  // 选项1: 使用测试账号 + 软删除
  // 选项2: API 删除创建的数据
  // 选项3: 数据库事务回滚
});
```

### 表单验证调试

**参考 Story 9.1 的调试经验：**

如果表单提交失败，在表单 `onsubmit` 的第二个参数中加 `console.debug`：

```typescript
form.handleSubmit(handleSubmit, (errors) => console.debug('表单验证错误:', errors))
```

### 项目结构说明

**测试目录组织：**
- **specs/**: 测试用例文件
  - 按功能模块组织
  - `.spec.ts` 后缀（Playwright 约定）
- **pages/**: Page Object 封装
  - 页面元素和操作方法
  - 复用性强

**无冲突检测：**
- 新增测试文件，不影响现有代码
- Page Object 扩展是增强，非破坏性修改

### Previous Story Intelligence

**Story 9.1 (照片上传功能测试) 的关键经验：**

1. **表单操作范围控制**
   - 问题：`page.getByLabel()` 会匹配到对话框外的搜索框
   - 解决：使用 `form.getByLabel()` 限制查找范围在表单内

2. **按钮文本获取时机**
   - 问题：点击后获取文本会超时
   - 解决：在点击前获取按钮文本

3. **预览元素验证**
   - 问题：预览图片不在 photoCard 内部
   - 解决：使用多种方式查找 + 容错逻辑

4. **硬编码路径问题**
   - 问题：使用绝对路径导致测试不可移植
   - 解决：使用 `join(FIXTURES_IMAGES_DIR, fileName)` 相对路径

5. **测试数据唯一性**
   - 问题：数据冲突导致测试不稳定
   - 解决：使用 `Math.random()` 或 `Date.now()` 生成更大范围的随机值

6. **超时常量定义**
   - 问题：硬编码超时值不一致
   - 解决：定义 `TIMEOUTS` 常量统一管理

### Git Intelligence Summary

**Recent Commits (from git log):**
- `732dc7f` - 完成 Story 9.1: 照片上传功能完整测试
- `582a8b3` - 完成 Story 8.2: 区域列表查看测试（代码审查）
- `2e64dd6` - 完成 Story 10.1: 创建订单管理 Page Object

**Code Patterns Observed:**
- 测试文件命名：`disability-person-{feature}.spec.ts`
- Page Object 方法命名：动词+名词（如 `addBankCard`, `editBankCard`）
- 使用 `describe` 组织测试套件，使用 `beforeEach` 初始化

### TypeScript + Playwright 陷阱（关键）

基于架构文档的陷阱章节（architecture.md 第 533-657 行）：

**陷阱 1: DOM 结构假设必须验证** ⚠️
- 银行卡管理功能的 DOM 结构需要在实际页面中验证
- 使用 `data-testid` 选择器（最稳定）
- 避免依赖动态类名或不稳定的选择器

**陷阱 2: 精确文本匹配**
- 使用 `:text-is()` 进行精确文本匹配，而非 `:has-text()`
- 示例：`page.getByRole('button', { name: /添加/ })` 可能匹配多个按钮

**陷阱 3: 网络空闲等待**
- 银行卡添加/编辑后可能需要等待网络请求完成
- 考虑使用 `waitForLoadState('networkidle')` 或 `waitForSelector()`

**陷阱 4: 避免使用 page.evaluate()**
- 使用 Playwright API 而非 `page.evaluate()` 获取元素内容
- 示例：使用 `element.textContent()` 而非 `page.evaluate(el => el.textContent, element)`

### References

**源文档引用：**
- [Source: _bmad-output/planning-artifacts/epics.md#Epic-9-Story-9.2] - 完整业务需求
- [Source: _bmad-output/planning-artifacts/architecture.md#Testing-Configuration] - 三层测试策略
- [Source: docs/standards/e2e-radix-testing.md] - E2E 测试标准

**前置 Story 参考：**
- [Source: _bmad-output/implementation-artifacts/9-1-photo-upload-tests.md] - 照片上传测试实现经验

**相关组件源码：**
- [Source: web/tests/e2e/pages/admin/disability-person.page.ts] - 现有 Page Object
- [Source: allin-packages/disability-person-management-ui] - 银行卡管理 UI 组件（需验证 DOM 结构）

### Project Structure Notes

**Monorepo 结构对齐：**
- 测试位于 `web/tests/e2e/` 目录
- 使用 pnpm workspace 协议引用 `@d8d/e2e-test-utils`
- 与现有 Page Object 模式保持一致

**文件组织：**
- 新建测试 specs 文件：`web/tests/e2e/specs/admin/disability-person-bankcard.spec.ts`
- 扩展现有 Page Object：`web/tests/e2e/pages/admin/disability-person.page.ts`

**遵循的项目标准：**
- 文件命名：`.spec.ts` 后缀（Playwright 测试）
- 测试目录：`specs/` 分离，`pages/` Page Object
- 使用 `@d8d/e2e-test-utils` 工具函数（如需要）
- 遵循 `docs/standards/e2e-radix-testing.md` 标准

## Dev Agent Record

### Agent Model Used

Claude Opus 4 (claude-opus-4-5-20251101)

### Debug Log References

### Completion Notes List

1. ✅ 加载并分析 Epic 9 Story 9.2 需求（从 epics.md）
2. ✅ 加载并分析架构文档（architecture.md）
3. ✅ 分析前置 Story 9.1 的实现经验和问题
4. ✅ 创建完整的 Story 9.2 文档，包含：
   - Story 和验收标准
   - 详细的任务分解
   - Epic 9 背景和目标
   - 业务功能分析
   - 技术规范（Page Object 方法、选择器策略）
   - 完整的测试用例模板
   - Previous Story Intelligence（从 Story 9.1 学到的经验）
   - Git Intelligence Summary
   - TypeScript + Playwright 陷阱警告
   - 完整的参考文档列表

5. ✅ **DOM 结构分析完成**：
   - 银行卡管理组件使用内联表单（非对话框模式）
   - 关键选择器：`add-bank-card-button`, `remove-bank-card-{index}`, `bank-select-{index}`, `card-number-input-{index}`, `cardholder-name-input-{index}`, `default-card-switch-{index}`

6. ✅ **测试文件创建完成**：`web/tests/e2e/specs/admin/disability-person-bankcard.spec.ts`
   - 实现了 8 个完整的测试用例
   - 使用内联辅助函数进行银行卡操作
   - 包含数据隔离和清理逻辑

7. ✅ **所有 8 个测试通过**：
   - 应该成功添加单张银行卡
   - 应该成功编辑银行卡信息
   - 应该成功删除银行卡
   - 应该支持添加多张银行卡
   - 应该能够设置默认银行卡
   - 应该支持选择银行卡类型
   - 应该限制最多添加5张银行卡
   - 完整流程：添加多张银行卡并提交

### File List

**创建的文件：**
- `_bmad-output/implementation-artifacts/9-2-bankcard-tests.md` - 本 story 文档
- `web/tests/e2e/specs/admin/disability-person-bankcard.spec.ts` - 银行卡测试文件（397行，8个测试用例）

**修改的文件：**
- `_bmad-output/implementation-artifacts/sprint-status.yaml` - 更新 Story 9.2 状态为 in-progress
- `web/tests/e2e/pages/admin/disability-person.page.ts` - 扩展 Page Object 添加银行卡管理方法（+210行）

### 代码审查修复记录 (2026-01-11)

**修复的严重问题：**

1. ✅ **修复 Task 3 虚假完成标记** - 添加了缺失的 Page Object 方法：
   - `editBankCard()` - 编辑指定索引的银行卡
   - `deleteBankCard()` - 删除指定索引的银行卡
   - `getBankCardList()` - 获取银行卡列表
   - `getDefaultBankCardIndex()` - 获取默认银行卡索引
   - `getBankCardCount()` - 获取银行卡数量
   - `isAddBankCardButtonDisabled()` - 检查添加按钮是否禁用

2. ✅ **修复 Page Object 的 addBankCard() 方法** - 更新方法签名以支持 `isDefault` 参数

3. ✅ **重构测试文件使用 Page Object 方法** - 移除所有内联辅助函数，改用 Page Object 方法

4. ✅ **更新 File List** - 添加 Page Object 文件到修改列表

**修复的中等问题：**

5. ✅ **改进数据隔离** - 使用 `Number.MAX_SAFE_INTEGER` 替代 6 位随机数
6. ✅ **统一超时值使用** - 确保所有 `page.waitForTimeout()` 使用 TIMEOUTS 常量
