Status: done
作为测试开发者, 我想要创建区域管理的 Page Object, 以便组织区域管理相关的页面元素和操作。
Given Epic 2 的 Page Object 模式已验证
When 创建 web/tests/e2e/pages/admin/region-management.page.ts
Then 定义区域列表页面的选择器和操作方法
And 定义添加区域对话框的选择器和操作方法
And 定义编辑区域对话框的选择器和操作方法
And 遵循现有 Page Object 设计模式
And 所有方法有完整的 TypeScript 类型定义
Given 区域管理 Page Object 已创建 When 编写区域列表查看测试用例 Then 验证区域列表按预期加载 And 验证区域数据的正确展示(名称、层级、状态等) And 验证分页功能(如适用) And 验证搜索功能(如适用) And 测试在真实浏览器中通过
Epic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)
这是 Epic B(区域管理业务测试)的第一个 Story。Epic 8 的目标是为区域管理功能建立完整的 E2E 测试覆盖,验证省/市/区/街道的添加、编辑、删除和级联选择功能。
依赖:
业务分组:
基于 web/tests/e2e/pages/admin/disability-person.page.ts 的成功经验,区域管理 Page Object 应遵循以下设计模式:
1. 类结构模式:
export class RegionManagementPage {
readonly page: Page;
// 页面级选择器
readonly pageTitle: Locator;
readonly addButton: Locator;
// ...其他选择器
constructor(page: Page) {
this.page = page;
// 初始化所有选择器
}
// 导航方法
async goto() { }
// 表单操作方法
async openAddDialog() { }
async fillRegionForm(data: RegionData) { }
async submitForm() { }
// 列表操作方法
async searchByName(name: string) { }
async regionExists(name: string): Promise<boolean> { }
}
2. 选择器定义模式:
page.getByRole() 优先(无障碍属性)page.getByLabel() 表单字段page.getByText() 或 page.locator() 作为兜底readonly Locator 类型3. 方法命名约定:
goto()expectToBeVisible(), expectSuccess()openAddDialog(), fillRegionForm()regionExists()基于现有项目架构和残疾人管理功能的经验,区域管理页面可能包含以下元素:
页面元素(需要验证实际 DOM 结构):
表单字段(需要验证实际 DOM 结构):
对话框元素(需要验证实际 DOM 结构):
根据 packages/e2e-test-utils/src/index.ts,以下工具可用于区域管理测试:
// Radix UI Select 工具
import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
// 省市区级联选择工具(非常适合区域管理)
import { selectCascade, selectProvinceCity } from '@d8d/e2e-test-utils';
// 文件上传工具(如果区域有图标上传)
import { uploadFileToField } from '@d8d/e2e-test-utils';
关键工具 - selectCascade:
/**
* 级联选择工具
* 用于省市区四级级联选择
*
* @param page - Playwright Page 对象
* @param selections - 选择项数组 [{label: '省份', value: '广东省'}, ...]
* @param options - 配置选项
*/
async function selectCascade(
page: Page,
selections: Array<{label: string, value: string}>,
options?: CascadeSelectOptions
): Promise<void>
目标文件位置:
web/tests/e2e/pages/admin/region-management.page.ts
导入路径:
import { Page, Locator } from '@playwright/test';
import { selectRadixOption, selectCascade } from '@d8d/e2e-test-utils';
测试文件位置(后续 Story):
web/tests/e2e/specs/admin/region-management.spec.ts
推荐定义以下类型:
/**
* 区域数据接口
*/
export interface RegionData {
/** 区域名称 */
name: string;
/** 区域代码 */
code?: string;
/** 区域层级(省/市/区/街道) */
level: 'province' | 'city' | 'district' | 'street';
/** 父级区域名称 */
parentRegion?: string;
/** 状态 */
status?: 'enabled' | 'disabled';
/** 备注 */
remark?: string;
}
/**
* 表单提交结果
*/
export interface FormSubmitResult {
success: boolean;
message?: string;
errorMessage?: string;
}
在实现 Page Object 之前,需要探索实际 DOM 结构:
启动开发服务器(如果未运行):
cd web && pnpm dev
导航到区域管理页面:
/admin/regions 或类似路径使用 Playwright Inspector 或浏览器开发者工具:
data-testid, role, aria-label记录发现:
| 方面 | 残疾人管理 | 区域管理 |
|---|---|---|
| 主要实体 | 残疾人个人 | 区域(省市区街道) |
| 树形结构 | 否 | 是(四级层级) |
| 级联选择 | 省市区三级 | 可能需要四级 |
| 图片上传 | 身份证、残疾证照片 | 可能无或区域图标 |
| 动态列表 | 银行卡、备注、回访 | 可能无 |
| 删除约束 | 较少 | 有子级区域不能删除 |
为支持未来的并行执行(Epic 9),考虑测试数据隔离:
/**
* 生成唯一区域名称(用于测试隔离)
*/
function generateUniqueRegionName(prefix: string = '测试区域'): string {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000);
return `${prefix}_${timestamp}_${random}`;
}
基于 Architecture.md 的 TypeScript + Playwright 陷阱:
DOM 结构假设必须验证 ⚠️
选择器策略优先级:
data-testid → aria-label + role → text contentdata-testid精确文本匹配:
:text-is() 而非 :has-text()超时配置:
DEFAULT_TIMEOUTS 常量避免使用 page.evaluate():
element.textContent() 而非 page.evaluate()Claude Opus 4 (claude-opus-4-5-20251101)
无调试问题
DOM 结构探索完成
AreaManagement.tsx 组件,理解了省市区树形管理页面的结构AreaForm.tsx 组件,理解了表单字段和对话框结构AreaTreeAsync.tsx 组件,理解了树形节点和操作按钮RegionManagementPage 类实现完成
web/tests/e2e/pages/admin/region-management.page.tsgoto(), expectToBeVisible())expandNode(), collapseNode(), regionExists(), getRegionStatus())fillRegionForm(), submitForm())createProvince(), createChildRegion(), editRegion(), deleteRegion(), toggleRegionStatus())TypeScript 类型定义完成
RegionData 接口(name, code, level, parentId, isDisabled)FormSubmitResult 接口(success, hasError, hasSuccess, errorMessage, successMessage)选择器策略验证
getByText('省市区树形管理')getByRole('button', { name: '新增省' }).border.rounded-lg.bg-background[role="dialog"], [role="alertdialog"][data-sonner-toast][data-type="success|error"]遵循设计模式
disability-person.page.ts 的代码风格page.evaluate()类型验证通过
web/tests/e2e/pages/admin/region-management.page.ts (新建)技术栈:
测试命令:
# 运行所有 E2E 测试
pnpm test:e2e:chromium
# 运行单个测试文件
pnpm test:e2e:chromium region-management
# 快速失败模式(调试时使用)
timeout 60 pnpm test:e2e:chromium region-management
包管理:
@d8d/e2e-test-utils@workspace:*命名约定:
region-management.page.ts)RegionManagementPage)goto(), searchByName())来自 Architecture.md 的关键决策:
选择器策略(混合策略优先级):
data-testid - 最高优先级aria-label + role - 无障碍标准错误处理策略:
E2ETestError 类(来自 e2e-test-utils)类型系统:
any 类型测试基础设施:
web/tests/e2e/specs/admin/web/tests/e2e/pages/admin/web/tests/e2e/fixtures/来自 Architecture.md "TypeScript + Playwright 常见陷阱" 部分:
⚠️ DOM 结构假设必须验证
✅ 正确做法:
// 使用 Playwright API 而非 page.evaluate()
const text = await element.textContent();
// 使用精确文本匹配
page.locator(`.option:text-is("广东省")`)
// 使用 data-testid 作为首选
page.getByTestId('region-name-input')
❌ 避免:
// 避免使用 page.evaluate()
const text = await page.evaluate(el => el.textContent, element);
// 避免部分文本匹配
page.locator(`.option:has-text("广东省")`)
基于 Architecture.md 的实现检查清单:
代码质量:
@internal 标记选择器策略:
data-testid 或 getByRole()配置和超时:
waitForLoadState('networkidle')DOM 操作:
page.evaluate()| 文档 | 路径 |
|---|---|
| PRD | _bmad-output/planning-artifacts/prd.md |
| Architecture | _bmad-output/planning-artifacts/architecture.md |
| Epics | _bmad-output/planning-artifacts/epics.md |
| Project Context | _bmad-output/project-context.md |
| 参考Page Object | web/tests/e2e/pages/admin/disability-person.page.ts |
| e2e-test-utils | packages/e2e-test-utils/src/index.ts |
前置 Epic:
当前 Epic (Epic 8):
后续 Epic:
Story ID: 8.1 Story Key: 8-1-region-page-object Epic: Epic 8 - 区域管理 E2E 测试 (Epic B) Status: done
交付物:
实现摘要:
RegionManagementPage Page Object 类RegionData、FormSubmitResult、NetworkResponse 类型接口REGION_LEVEL 和 REGION_STATUS 常量替代魔法数字代码审查修复 (2026-01-11):
下一步操作: