# Story 8.1: 创建区域管理 Page Object Status: done ## Story 作为测试开发者, 我想要创建区域管理的 Page Object, 以便组织区域管理相关的页面元素和操作。 ## Acceptance Criteria **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** 测试在真实浏览器中通过 ## Tasks / Subtasks - [x] 创建 RegionManagementPage 类基础结构 (AC: #) - [x] 定义页面选择器(页面标题、新增按钮、表格等) - [x] 实现构造函数和初始化逻辑 - [x] 实现页面导航方法 (AC: #) - [x] 实现 goto() 方法导航到区域管理页面 - [x] 实现 expectToBeVisible() 验证页面元素可见性 - [x] 实现区域列表相关方法 (AC: #) - [x] 定义列表表格、搜索输入框、搜索按钮等选择器 - [x] 实现 regionExists() 验证区域是否存在 - [x] 实现 expandNode() / collapseNode() 节点展开收起方法 - [x] 实现 getRegionStatus() 获取区域状态方法 - [x] 实现添加区域对话框方法 (AC: #) - [x] 定义添加区域对话框选择器 - [x] 实现 openCreateProvinceDialog() 打开新增省对话框 - [x] 实现 openAddChildDialog() 打开新增子区域对话框 - [x] 实现 fillRegionForm() 填写区域表单 - [x] 实现 submitForm() 提交表单 - [x] 实现编辑区域对话框方法 (AC: #) - [x] 定义编辑区域对话框选择器 - [x] 实现 openEditDialog() 打开编辑对话框 - [x] 实现 editRegion() 区域编辑方法 - [x] 实现删除区域相关方法 (AC: #) - [x] 定义删除确认对话框选择器 - [x] 实现 openDeleteDialog() 打开删除对话框 - [x] 实现 confirmDelete() 确认删除 - [x] 实现 cancelDelete() 取消删除 - [x] 实现 deleteRegion() 删除区域方法 - [x] 实现状态切换方法 (AC: #) - [x] 实现 openToggleStatusDialog() 打开状态切换对话框 - [x] 实现 confirmToggleStatus() 确认状态切换 - [x] 实现 cancelToggleStatus() 取消状态切换 - [x] 实现 toggleRegionStatus() 切换区域状态方法 - [x] 添加 TypeScript 类型定义 (AC: #) - [x] 定义 RegionData 接口 - [x] 定义 FormSubmitResult 接口 - [x] 定义所有方法的参数和返回值类型 - [x] 遵循现有 Page Object 设计模式 (AC: #) - [x] 参考 disability-person.page.ts 的代码风格 - [x] 使用一致的命名约定 - [x] 使用 JSDoc 注释 - [x] 优先使用 Playwright API 而非 page.evaluate() ## Dev Notes ### Epic 8 背景和上下文 **Epic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)** 这是 Epic B(区域管理业务测试)的第一个 Story。Epic 8 的目标是为区域管理功能建立完整的 E2E 测试覆盖,验证省/市/区/街道的添加、编辑、删除和级联选择功能。 **依赖:** - Epic 1: ✅ 已完成(Select 工具基础框架) - Epic 2: ✅ 已完成(Select 工具在真实 E2E 测试中验证) - Epic 3: ✅ 已完成(文件上传工具、级联选择工具) **业务分组:** - Epic A(残疾人管理)- 已完成基础工具验证 - Epic B(区域管理)- 当前目标 - Epic C(e2e-test-utils 包维护)- 支持性任务 ### Page Object 设计模式参考 基于 `web/tests/e2e/pages/admin/disability-person.page.ts` 的成功经验,区域管理 Page Object 应遵循以下设计模式: **1. 类结构模式:** ```typescript 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 { } } ``` **2. 选择器定义模式:** - 使用 `page.getByRole()` 优先(无障碍属性) - 使用 `page.getByLabel()` 表单字段 - 使用 `page.getByText()` 或 `page.locator()` 作为兜底 - 所有选择器定义为 `readonly Locator` 类型 **3. 方法命名约定:** - 导航方法: `goto()` - 断言方法: `expectToBeVisible()`, `expectSuccess()` - 操作方法: 动词开头,如 `openAddDialog()`, `fillRegionForm()` - 查询方法: 返回 boolean,如 `regionExists()` ### 区域管理功能分析 基于现有项目架构和残疾人管理功能的经验,区域管理页面可能包含以下元素: **页面元素(需要验证实际 DOM 结构):** - 页面标题: "区域管理" - 新增按钮: "新增区域" 或类似文本 - 搜索输入框: 占位符可能为 "搜索区域名称" - 搜索按钮: "搜索" 或图标按钮 - 数据表格: 显示区域列表 - 列: 区域名称、区域代码、层级(省/市/区/街道)、状态、操作 **表单字段(需要验证实际 DOM 结构):** - 区域名称: 文本输入框 - 区域代码: 文本输入框 - 区域层级: Radix UI Select(省/市/区/街道) - 父级区域: 级联 Radix UI Select(选择上级区域) - 状态: Radix UI Select 或 Switch(启用/禁用) - 备注: 文本域 **对话框元素(需要验证实际 DOM 结构):** - 添加区域对话框 - 编辑区域对话框 - 删除确认对话框 ### 可用的 e2e-test-utils 工具 根据 `packages/e2e-test-utils/src/index.ts`,以下工具可用于区域管理测试: ```typescript // 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:** ```typescript /** * 级联选择工具 * 用于省市区四级级联选择 * * @param page - Playwright Page 对象 * @param selections - 选择项数组 [{label: '省份', value: '广东省'}, ...] * @param options - 配置选项 */ async function selectCascade( page: Page, selections: Array<{label: string, value: string}>, options?: CascadeSelectOptions ): Promise ``` ### 项目结构笔记 **目标文件位置:** ``` web/tests/e2e/pages/admin/region-management.page.ts ``` **导入路径:** ```typescript import { Page, Locator } from '@playwright/test'; import { selectRadixOption, selectCascade } from '@d8d/e2e-test-utils'; ``` **测试文件位置(后续 Story):** ``` web/tests/e2e/specs/admin/region-management.spec.ts ``` ### TypeScript 类型定义 推荐定义以下类型: ```typescript /** * 区域数据接口 */ 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; } ``` ### DOM 结构探索步骤 在实现 Page Object 之前,需要探索实际 DOM 结构: 1. **启动开发服务器**(如果未运行): ```bash cd web && pnpm dev ``` 2. **导航到区域管理页面**: - URL: `/admin/regions` 或类似路径 3. **使用 Playwright Inspector 或浏览器开发者工具**: - 检查页面元素的 `data-testid`, `role`, `aria-label` - 记录表单字段的选择器策略 - 验证 Radix UI Select 的 DOM 结构 4. **记录发现**: - 对话框触发器的选择器 - 表单字段的 label 文本 - 表格的 CSS 类或结构 - Toast 消息的选择器 ### 与残疾人管理 Page Object 的主要差异 | 方面 | 残疾人管理 | 区域管理 | |------|-----------|----------| | 主要实体 | 残疾人个人 | 区域(省市区街道) | | 树形结构 | 否 | 是(四级层级) | | 级联选择 | 省市区三级 | 可能需要四级 | | 图片上传 | 身份证、残疾证照片 | 可能无或区域图标 | | 动态列表 | 银行卡、备注、回访 | 可能无 | | 删除约束 | 较少 | 有子级区域不能删除 | ### 测试隔离策略 为支持未来的并行执行(Epic 9),考虑测试数据隔离: ```typescript /** * 生成唯一区域名称(用于测试隔离) */ function generateUniqueRegionName(prefix: string = '测试区域'): string { const timestamp = Date.now(); const random = Math.floor(Math.random() * 1000); return `${prefix}_${timestamp}_${random}`; } ``` ### 常见陷阱和注意事项 **基于 Architecture.md 的 TypeScript + Playwright 陷阱:** 1. **DOM 结构假设必须验证 ⚠️** - 不能基于理想模型开发选择器 - 必须在真实组件上验证 DOM 结构 - Radix UI Select 的 DOM 可能随版本变化 2. **选择器策略优先级:** - `data-testid` → `aria-label` + role → text content - 推荐在组件上添加 `data-testid` 3. **精确文本匹配:** - 使用 `:text-is()` 而非 `:has-text()` - 避免部分匹配导致误选 4. **超时配置:** - 使用 `DEFAULT_TIMEOUTS` 常量 - 网络空闲等待使用用户自定义超时值 5. **避免使用 page.evaluate():** - 优先使用 Playwright API - 使用 `element.textContent()` 而非 `page.evaluate()` ## Dev Agent Record ### Agent Model Used Claude Opus 4 (claude-opus-4-5-20251101) ### Debug Log References 无调试问题 ### Completion Notes List 1. **DOM 结构探索完成** - 分析了 `AreaManagement.tsx` 组件,理解了省市区树形管理页面的结构 - 分析了 `AreaForm.tsx` 组件,理解了表单字段和对话框结构 - 分析了 `AreaTreeAsync.tsx` 组件,理解了树形节点和操作按钮 2. **RegionManagementPage 类实现完成** - 创建了 `web/tests/e2e/pages/admin/region-management.page.ts` - 实现了完整的页面导航方法 (`goto()`, `expectToBeVisible()`) - 实现了区域树操作方法 (`expandNode()`, `collapseNode()`, `regionExists()`, `getRegionStatus()`) - 实现了对话框操作方法(新增省、新增子区域、编辑、删除、状态切换) - 实现了表单操作方法 (`fillRegionForm()`, `submitForm()`) - 实现了快捷方法 (`createProvince()`, `createChildRegion()`, `editRegion()`, `deleteRegion()`, `toggleRegionStatus()`) 3. **TypeScript 类型定义完成** - 定义了 `RegionData` 接口(name, code, level, parentId, isDisabled) - 定义了 `FormSubmitResult` 接口(success, hasError, hasSuccess, errorMessage, successMessage) - 所有方法都有完整的参数和返回值类型定义 4. **选择器策略验证** - 页面标题: `getByText('省市区树形管理')` - 新增按钮: `getByRole('button', { name: '新增省' })` - 树形容器: `.border.rounded-lg.bg-background` - 对话框: `[role="dialog"]`, `[role="alertdialog"]` - Toast 消息: `[data-sonner-toast][data-type="success|error"]` 5. **遵循设计模式** - 参考了 `disability-person.page.ts` 的代码风格 - 使用了 consistent 命名约定(PascalCase 类名,camelCase 方法名) - 添加了完整的 JSDoc 注释 - 优先使用 Playwright API 而非 `page.evaluate()` 6. **类型验证通过** - 无 TypeScript 类型错误 - 代码符合项目 ESLint 规范 ### File List - `web/tests/e2e/pages/admin/region-management.page.ts` (新建) ## Project Context Reference ### 关键项目规则摘要 **技术栈:** - Playwright 1.55.0 - E2E 测试框架 - TypeScript 5.9.3 - 严格模式 - @d8d/e2e-test-utils - 内部测试工具包 **测试命令:** ```bash # 运行所有 E2E 测试 pnpm test:e2e:chromium # 运行单个测试文件 pnpm test:e2e:chromium region-management # 快速失败模式(调试时使用) timeout 60 pnpm test:e2e:chromium region-management ``` **包管理:** - 使用 pnpm(版本 10.18.3) - 内部包使用 workspace 协议: `@d8d/e2e-test-utils@workspace:*` **命名约定:** - 文件名: kebab-case (如: `region-management.page.ts`) - 类名: PascalCase (如: `RegionManagementPage`) - 函数/变量: camelCase (如: `goto()`, `searchByName()`) ### 必须遵循的架构决策 **来自 Architecture.md 的关键决策:** 1. **选择器策略(混合策略优先级):** - `data-testid` - 最高优先级 - `aria-label` + role - 无障碍标准 - Text content + role - 兜底方案 2. **错误处理策略:** - 使用 `E2ETestError` 类(来自 e2e-test-utils) - 包含完整 ErrorContext 3. **类型系统:** - 所有方法必须有完整的 TypeScript 类型定义 - 禁止使用 `any` 类型 4. **测试基础设施:** - 测试文件位置: `web/tests/e2e/specs/admin/` - Page Object 位置: `web/tests/e2e/pages/admin/` - Fixtures 位置: `web/tests/e2e/fixtures/` ### TypeScript + Playwright 陷阱预防 **来自 Architecture.md "TypeScript + Playwright 常见陷阱" 部分:** ⚠️ **DOM 结构假设必须验证** - 不能基于理想模型开发选择器 - 必须在真实组件上验证 DOM 结构 - 单元测试无法发现真实 DOM 问题 ✅ **正确做法:** ```typescript // 使用 Playwright API 而非 page.evaluate() const text = await element.textContent(); // 使用精确文本匹配 page.locator(`.option:text-is("广东省")`) // 使用 data-testid 作为首选 page.getByTestId('region-name-input') ``` ❌ **避免:** ```typescript // 避免使用 page.evaluate() const text = await page.evaluate(el => el.textContent, element); // 避免部分文本匹配 page.locator(`.option:has-text("广东省")`) ``` ### 代码质量检查清单 基于 Architecture.md 的实现检查清单: **代码质量:** - [ ] 所有导出方法都有完整的 JSDoc - [ ] 内部方法使用 `@internal` 标记 - [ ] 错误处理提供友好消息 **选择器策略:** - [ ] 优先使用 `data-testid` 或 `getByRole()` - [ ] 文本选择器使用精确匹配 - [ ] DOM 结构基于真实组件验证 **配置和超时:** - [ ] 超时值使用合理配置 - [ ] 网络操作使用 `waitForLoadState('networkidle')` **DOM 操作:** - [ ] 避免使用 `page.evaluate()` - [ ] 优先使用 Playwright API ### 参考文档位置 | 文档 | 路径 | |------|------| | 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 和 Story **前置 Epic:** - Epic 1: ✅ 完成 - Select 工具基础框架 - Epic 2: ✅ 完成 - Select 工具在真实 E2E 测试中验证 - Epic 3: ✅ 完成 - 文件上传工具、级联选择工具 **当前 Epic (Epic 8):** - Story 8.1: 📝 当前 - 创建区域管理 Page Object - Story 8.2: ⏳ 待开始 - 编写区域列表查看测试 - Story 8.3: ⏳ 待开始 - 编写添加区域测试 - Story 8.4: ⏳ 待开始 - 编写编辑区域测试 - Story 8.5: ⏳ 待开始 - 编写删除区域测试 - Story 8.6: ⏳ 待开始 - 编写级联选择完整流程测试 **后续 Epic:** - Epic 9: 🔄 进行中 - 残疾人管理完整 E2E 测试覆盖 ## Completion Status **Story ID:** 8.1 **Story Key:** 8-1-region-page-object **Epic:** Epic 8 - 区域管理 E2E 测试 (Epic B) **Status:** done **交付物:** - [x] Story 文档创建完成 - [x] RegionManagementPage 类实现 - [x] TypeScript 类型定义 - [x] DOM 结构探索和验证 - [x] 所有任务和子任务已完成 - [x] 代码审查完成并修复所有 HIGH/MEDIUM 问题 **实现摘要:** - 创建了 `RegionManagementPage` Page Object 类 - 实现了页面导航、区域树操作、对话框操作、表单操作等方法 - 定义了 `RegionData`、`FormSubmitResult`、`NetworkResponse` 类型接口 - 添加了 `REGION_LEVEL` 和 `REGION_STATUS` 常量替代魔法数字 - 所有方法都有完整的 JSDoc 注释和 TypeScript 类型定义 - 遵循了现有 Page Object 设计模式 - 通过了 TypeScript 类型检查 **代码审查修复 (2026-01-11):** - [x] 添加文件到 Git 追踪 - [x] 改进选择器策略(使用精确文本匹配、xpath 祖先定位) - [x] 添加网络请求监听和响应捕获到 submitForm() - [x] 修复状态切换按钮选择器(使用更精确的 xpath 定位) - [x] 修复展开/收起节点选择器(不依赖 data-lucide 属性) - [x] 改进 getRegionStatus 选择器精度 - [x] 改进 waitForTreeLoaded 选择器(使用 .text-muted-foreground 类) - [x] 添加 REGION_LEVEL 和 REGION_STATUS 常量 - [x] 添加 NetworkResponse 接口用于网络响应数据 **下一步操作:** 1. 编写区域列表查看测试(Story 8.2)