Status: done
作为测试开发者, 我想要编写银行卡管理功能的测试, 以便验证银行卡的添加、编辑、删除功能。
Given 残疾人管理 Page Object 已存在 When 编写银行卡管理测试 Then 包含以下测试场景:
添加银行卡
编辑银行卡
删除银行卡
多张银行卡管理
[x] Task 1: 分析银行卡管理功能的 DOM 结构 (AC: #1, #2, #3, #4)
[x] Task 2: 创建银行卡测试文件 (AC: #1, #2, #3, #4)
web/tests/e2e/specs/admin/disability-person-bankcard.spec.ts[x] Task 3: 更新 Page Object (AC: #1, #2, #3, #4)
DisabilityPersonManagementPageaddBankCard() 方法editBankCard() 方法deleteBankCard() 方法getBankCardList() 方法用于验证[x] Task 4: 运行测试并验证通过 (AC: #1, #2, #3, #4)
pnpm test:e2e:chromium disability-person-bankcard 运行测试Epic 9: 残疾人管理完整 E2E 测试覆盖(含并行隔离)
为残疾人管理功能编写完整的、真正验证业务功能的 E2E 测试,并确保测试可以与未来的区域管理测试并行运行。
Epic 9 的 Story 依赖关系:
银行卡管理功能概述:
残疾人管理系统中,每个残疾人可以关联多张银行卡,用于管理残疾人的工资发放、补贴发放等信息。
典型功能流程:
当前 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(需扩展)
测试文件模板:
// 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('默认');
});
});
需要在 DisabilityPersonManagementPage 中添加的方法:
/**
* 添加银行卡
* @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 的经验:
page.getByRole('button', { name: /添加.*银行卡/ })form.getByLabel() 限制范围data-testid 或 role="listitem" 选择器示例:
// 点击编辑按钮(第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 的数据隔离模式:
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:
form.handleSubmit(handleSubmit, (errors) => console.debug('表单验证错误:', errors))
测试目录组织:
.spec.ts 后缀(Playwright 约定)无冲突检测:
Story 9.1 (照片上传功能测试) 的关键经验:
表单操作范围控制
page.getByLabel() 会匹配到对话框外的搜索框form.getByLabel() 限制查找范围在表单内按钮文本获取时机
预览元素验证
硬编码路径问题
join(FIXTURES_IMAGES_DIR, fileName) 相对路径测试数据唯一性
Math.random() 或 Date.now() 生成更大范围的随机值超时常量定义
TIMEOUTS 常量统一管理Recent Commits (from git log):
732dc7f - 完成 Story 9.1: 照片上传功能完整测试582a8b3 - 完成 Story 8.2: 区域列表查看测试(代码审查)2e64dd6 - 完成 Story 10.1: 创建订单管理 Page ObjectCode Patterns Observed:
disability-person-{feature}.spec.tsaddBankCard, editBankCard)describe 组织测试套件,使用 beforeEach 初始化基于架构文档的陷阱章节(architecture.md 第 533-657 行):
陷阱 1: DOM 结构假设必须验证 ⚠️
data-testid 选择器(最稳定)陷阱 2: 精确文本匹配
:text-is() 进行精确文本匹配,而非 :has-text()page.getByRole('button', { name: /添加/ }) 可能匹配多个按钮陷阱 3: 网络空闲等待
waitForLoadState('networkidle') 或 waitForSelector()陷阱 4: 避免使用 page.evaluate()
page.evaluate() 获取元素内容element.textContent() 而非 page.evaluate(el => el.textContent, element)源文档引用:
前置 Story 参考:
相关组件源码:
Monorepo 结构对齐:
web/tests/e2e/ 目录@d8d/e2e-test-utils文件组织:
web/tests/e2e/specs/admin/disability-person-bankcard.spec.tsweb/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 标准Claude Opus 4 (claude-opus-4-5-20251101)
✅ 创建完整的 Story 9.2 文档,包含:
✅ DOM 结构分析完成:
add-bank-card-button, remove-bank-card-{index}, bank-select-{index}, card-number-input-{index}, cardholder-name-input-{index}, default-card-switch-{index}✅ 测试文件创建完成:web/tests/e2e/specs/admin/disability-person-bankcard.spec.ts
✅ 所有 8 个测试通过:
创建的文件:
_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-progressweb/tests/e2e/pages/admin/disability-person.page.ts - 扩展 Page Object 添加银行卡管理方法(+210行)修复的严重问题:
✅ 修复 Task 3 虚假完成标记 - 添加了缺失的 Page Object 方法:
editBankCard() - 编辑指定索引的银行卡deleteBankCard() - 删除指定索引的银行卡getBankCardList() - 获取银行卡列表getDefaultBankCardIndex() - 获取默认银行卡索引getBankCardCount() - 获取银行卡数量isAddBankCardButtonDisabled() - 检查添加按钮是否禁用✅ 修复 Page Object 的 addBankCard() 方法 - 更新方法签名以支持 isDefault 参数
✅ 重构测试文件使用 Page Object 方法 - 移除所有内联辅助函数,改用 Page Object 方法
✅ 更新 File List - 添加 Page Object 文件到修改列表
修复的中等问题:
Number.MAX_SAFE_INTEGER 替代 6 位随机数page.waitForTimeout() 使用 TIMEOUTS 常量