|
@@ -0,0 +1,682 @@
|
|
|
|
|
+# Story 8.6: 编写级联选择完整流程测试
|
|
|
|
|
+
|
|
|
|
|
+Status: ready-for-dev
|
|
|
|
|
+
|
|
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
|
+
|
|
|
|
|
+## Story
|
|
|
|
|
+
|
|
|
|
|
+作为测试开发者,
|
|
|
|
|
+我想要编写完整的四级级联选择测试,
|
|
|
|
|
+以便验证省/市/区/街道联动的完整场景。
|
|
|
|
|
+
|
|
|
|
|
+## Acceptance Criteria
|
|
|
|
|
+
|
|
|
|
|
+**Given** 单独的区域操作测试已通过
|
|
|
|
|
+**When** 编写完整的级联选择流程测试
|
|
|
|
|
+**Then** 从选择省份开始,依次选择市、区、街道
|
|
|
|
|
+**And** 验证每级选择后,下一级选项正确加载
|
|
|
|
|
+**And** 验证上级变更时,下级选择被清空
|
|
|
|
|
+**And** 验证完整的添加流程(省份→城市→区域→街道)
|
|
|
|
|
+**And** 测试在真实浏览器中通过
|
|
|
|
|
+
|
|
|
|
|
+## Tasks / Subtasks
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 创建测试文件基础结构
|
|
|
|
|
+ - [ ] 创建 `web/tests/e2e/specs/admin/region-cascade.spec.ts`
|
|
|
|
|
+ - [ ] 配置 test fixtures(adminLoginPage, regionManagementPage)
|
|
|
|
|
+ - [ ] 设置测试组和 beforeEach/afterEach 钩子
|
|
|
|
|
+- [ ] 实现完整四级级联选择测试
|
|
|
|
|
+ - [ ] 测试省→市→区的完整选择流程
|
|
|
|
|
+ - [ ] 验证每级选择后树形结构正确更新
|
|
|
|
|
+ - [ ] 验证选择后的区域出现在树中
|
|
|
|
|
+- [ ] 实现上级变更时下级清空的验证
|
|
|
|
|
+ - [ ] 测试修改省份后市级选择被清空
|
|
|
|
|
+ - [ ] 测试修改城市后区级选择被清空
|
|
|
|
|
+- [ ] 实现测试数据隔离
|
|
|
|
|
+ - [ ] 每个测试使用唯一的区域名称
|
|
|
|
|
+ - [ ] 测试后清理测试数据
|
|
|
|
|
+
|
|
|
|
|
+## Dev Notes
|
|
|
|
|
+
|
|
|
|
|
+### Epic 8 背景和上下文
|
|
|
|
|
+
|
|
|
|
|
+**Epic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)**
|
|
|
|
|
+
|
|
|
|
|
+这是 Epic B(区域管理业务测试)的第六个 Story。前置 Story 已完成:
|
|
|
|
|
+- Story 8.1: ✅ 已完成 - RegionManagementPage Page Object
|
|
|
|
|
+- Story 8.2: ✅ 已完成 - 区域列表查看测试
|
|
|
|
|
+- Story 8.3: ✅ 已完成 - 添加区域测试
|
|
|
|
|
+- Story 8.4: ✅ 已完成 - 编辑区域测试
|
|
|
|
|
+- Story 8.5: ✅ 已完成 - 删除区域测试
|
|
|
|
|
+
|
|
|
|
|
+**依赖:**
|
|
|
|
|
+- Epic 1: ✅ 已完成(Select 工具基础框架)
|
|
|
|
|
+- Epic 2: ✅ 已完成(Select 工具在真实 E2E 测试中验证)
|
|
|
|
|
+- Story 8.1: ✅ 已完成(RegionManagementPage Page Object)
|
|
|
|
|
+
|
|
|
|
|
+### 级联选择功能概述
|
|
|
|
|
+
|
|
|
|
|
+区域管理采用**树形结构设计**,而非传统的下拉框级联选择。这是两种不同的 UI 设计模式:
|
|
|
|
|
+
|
|
|
|
|
+**树形结构模式(当前实现):**
|
|
|
|
|
+- 点击父节点的"新增市/区"按钮来确定父级
|
|
|
|
|
+- 父级关系通过树形节点层级体现,而非下拉框选择
|
|
|
|
|
+- 新增子区域时通过点击父节点上的操作按钮来关联
|
|
|
|
|
+
|
|
|
|
|
+**传统下拉框模式(未使用):**
|
|
|
|
|
+- 表单中使用下拉框选择父级区域
|
|
|
|
|
+- 需要使用 `selectRadixOption` 或 `selectRadixOptionAsync`
|
|
|
|
|
+- 这是 Epic 原始设计中假设的模式,但实际 UI 使用了树形结构
|
|
|
|
|
+
|
|
|
|
|
+⚠️ **重要说明:** 本 Story 的验收标准中提到的"级联选择"测试,在当前树形结构 UI 中,体现为**逐级创建子区域并验证树形结构正确显示**的测试场景。
|
|
|
|
|
+
|
|
|
|
|
+### RegionManagementPage API 参考
|
|
|
|
|
+
|
|
|
|
|
+**级联选择相关方法(来自 Story 8.1):**
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 创建省份(第一级)
|
|
|
|
|
+await regionManagementPage.createProvince({ name: '测试省', code: '110000' });
|
|
|
|
|
+
|
|
|
|
|
+// 创建市级子区域(第二级)
|
|
|
|
|
+await regionManagementPage.createChildRegion('测试省', '市', { name: '测试市', code: '110100' });
|
|
|
|
|
+
|
|
|
|
|
+// 创建区级子区域(第三级)
|
|
|
|
|
+await regionManagementPage.createChildRegion('测试市', '区', { name: '测试区', code: '110101' });
|
|
|
|
|
+
|
|
|
|
|
+// 展开节点查看子区域
|
|
|
|
|
+await regionManagementPage.expandNode('测试省');
|
|
|
|
|
+await regionManagementPage.expandNode('测试市');
|
|
|
|
|
+
|
|
|
|
|
+// 验证区域是否存在
|
|
|
|
|
+const exists = await regionManagementPage.regionExists('测试区');
|
|
|
|
|
+// 返回: boolean
|
|
|
|
|
+
|
|
|
|
|
+// 等待树形结构加载
|
|
|
|
|
+await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**节点操作方法:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 展开节点
|
|
|
|
|
+await regionManagementPage.expandNode('区域名称');
|
|
|
|
|
+
|
|
|
|
|
+// 收起节点
|
|
|
|
|
+await regionManagementPage.collapseNode('区域名称');
|
|
|
|
|
+
|
|
|
|
|
+// 获取区域状态
|
|
|
|
|
+const status = await regionManagementPage.getRegionStatus('区域名称');
|
|
|
|
|
+// 返回: '启用' | '禁用' | null
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 测试文件结构模式
|
|
|
|
|
+
|
|
|
|
|
+参考 `web/tests/e2e/specs/admin/region-add.spec.ts`(Story 8.3)的成功模式:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+import { test, expect } from '../../utils/test-setup';
|
|
|
|
|
+import { readFileSync } from 'fs';
|
|
|
|
|
+import { join, dirname } from 'path';
|
|
|
|
|
+import { fileURLToPath } from 'url';
|
|
|
|
|
+
|
|
|
|
|
+const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
+const __dirname = dirname(__filename);
|
|
|
|
|
+const testUsers = JSON.parse(readFileSync(join(__dirname, '../../fixtures/test-users.json'), 'utf-8'));
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 生成唯一区域名称
|
|
|
|
|
+ */
|
|
|
|
|
+function generateUniqueRegionName(prefix: string = '测试区域'): string {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const random = Math.floor(Math.random() * 1000);
|
|
|
|
|
+ return `${prefix}_${timestamp}_${random}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+test.describe.serial('级联选择完整流程测试', () => {
|
|
|
|
|
+ const createdProvinces: string[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ test.beforeEach(async ({ adminLoginPage, regionManagementPage }) => {
|
|
|
|
|
+ // 登录
|
|
|
|
|
+ await adminLoginPage.goto();
|
|
|
|
|
+ await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
|
|
|
|
|
+ await adminLoginPage.expectLoginSuccess();
|
|
|
|
|
+
|
|
|
|
|
+ // 导航到区域管理页面
|
|
|
|
|
+ await regionManagementPage.goto();
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test.afterEach(async ({ regionManagementPage }) => {
|
|
|
|
|
+ // 清理测试数据
|
|
|
|
|
+ for (const provinceName of createdProvinces) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await regionManagementPage.deleteRegion(provinceName);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.debug('清理测试数据失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ createdProvinces.length = 0;
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 测试用例设计
|
|
|
|
|
+
|
|
|
|
|
+**1. 完整三级级联选择测试(省→市→区):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+test.describe('三级级联选择(省市区)', () => {
|
|
|
|
|
+ test('应该成功创建完整的省市区三级结构', async ({ regionManagementPage }) => {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const provinceName = `测试省_${timestamp}`;
|
|
|
|
|
+ const cityName = `测试市_${timestamp}`;
|
|
|
|
|
+ const districtName = `测试区_${timestamp}`;
|
|
|
|
|
+
|
|
|
|
|
+ // Step 1: 创建省份
|
|
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(provinceName)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ // Step 2: 创建市级子区域
|
|
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+
|
|
|
|
|
+ // 展开省份验证市在正确的层级
|
|
|
|
|
+ await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(cityName)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ // Step 3: 创建区级子区域
|
|
|
|
|
+ await regionManagementPage.createChildRegion(cityName, '区', { name: districtName });
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+
|
|
|
|
|
+ // 展开市验证区在正确的层级
|
|
|
|
|
+ await regionManagementPage.expandNode(cityName);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(districtName)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到清理列表
|
|
|
|
|
+ createdProvinces.push(provinceName);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('应该正确显示三级树形结构', async ({ regionManagementPage }) => {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const provinceName = `级联省_${timestamp}`;
|
|
|
|
|
+ const cityName = `级联市_${timestamp}`;
|
|
|
|
|
+ const districtName = `级联区_${timestamp}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建三级结构
|
|
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(cityName, '区', { name: districtName });
|
|
|
|
|
+
|
|
|
|
|
+ // 展开所有节点
|
|
|
|
|
+ await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+ await regionManagementPage.expandNode(cityName);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证所有层级都可见
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(provinceName)).toBe(true);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(cityName)).toBe(true);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(districtName)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ createdProvinces.push(provinceName);
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**2. 多个子区域级联测试:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+test.describe('多个子区域级联', () => {
|
|
|
|
|
+ test('应该支持一个省份下多个市', async ({ regionManagementPage }) => {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const provinceName = `多市省_${timestamp}`;
|
|
|
|
|
+ const city1Name = `市1_${timestamp}`;
|
|
|
|
|
+ const city2Name = `市2_${timestamp}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建省份
|
|
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
|
|
+
|
|
|
|
|
+ // 创建多个市
|
|
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: city1Name });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: city2Name });
|
|
|
|
|
+
|
|
|
|
|
+ // 展开省份验证
|
|
|
|
|
+ await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(city1Name)).toBe(true);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(city2Name)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ createdProvinces.push(provinceName);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ test('应该支持一个市下多个区', async ({ regionManagementPage }) => {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const provinceName = `多区省_${timestamp}`;
|
|
|
|
|
+ const cityName = `多区市_${timestamp}`;
|
|
|
|
|
+ const district1Name = `区1_${timestamp}`;
|
|
|
|
|
+ const district2Name = `区2_${timestamp}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建省市区
|
|
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(cityName, '区', { name: district1Name });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(cityName, '区', { name: district2Name });
|
|
|
|
|
+
|
|
|
|
|
+ // 展开验证
|
|
|
|
|
+ await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+ await regionManagementPage.expandNode(cityName);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(district1Name)).toBe(true);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(district2Name)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ createdProvinces.push(provinceName);
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**3. 级联编辑场景测试(上级变更):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+test.describe('级联编辑场景', () => {
|
|
|
|
|
+ test('编辑区域应保持父子关系', async ({ regionManagementPage }) => {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const provinceName = `编辑省_${timestamp}`;
|
|
|
|
|
+ const cityName = `编辑市_${timestamp}`;
|
|
|
|
|
+ const newCityName = `新市_${timestamp}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建省和市
|
|
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+
|
|
|
|
|
+ // 编辑市名称
|
|
|
|
|
+ await regionManagementPage.editRegion(cityName, { name: newCityName });
|
|
|
|
|
+
|
|
|
|
|
+ // 验证编辑成功
|
|
|
|
|
+ await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(newCityName)).toBe(true);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(cityName)).toBe(false);
|
|
|
|
|
+
|
|
|
|
|
+ createdProvinces.push(provinceName);
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**4. 深层级级联测试(省→市→区→多个区):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+test.describe('深层级级联', () => {
|
|
|
|
|
+ test('应该支持深层级联选择', async ({ regionManagementPage }) => {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const provinceName = `深层省_${timestamp}`;
|
|
|
|
|
+ const cityName = `深层市_${timestamp}`;
|
|
|
|
|
+ const district1Name = `深层区1_${timestamp}`;
|
|
|
|
|
+ const district2Name = `深层区2_${timestamp}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建深层级结构
|
|
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(cityName, '区', { name: district1Name });
|
|
|
|
|
+ await regionManagementPage.createChildRegion(cityName, '区', { name: district2Name });
|
|
|
|
|
+
|
|
|
|
|
+ // 逐级展开验证
|
|
|
|
|
+ await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(cityName)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ await regionManagementPage.expandNode(cityName);
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(district1Name)).toBe(true);
|
|
|
|
|
+ expect(await regionManagementPage.regionExists(district2Name)).toBe(true);
|
|
|
|
|
+
|
|
|
|
|
+ createdProvinces.push(provinceName);
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 与前序 Story 的关键差异
|
|
|
|
|
+
|
|
|
|
|
+| 方面 | Story 8.5(删除区域) | Story 8.6(级联选择) |
|
|
|
|
|
+|------|---------------------|---------------------|
|
|
|
|
|
+| 主要操作 | 删除数据 | 创建层级数据 |
|
|
|
|
|
+| 测试重点 | 级联约束(有子级不可删) | 层级结构正确性 |
|
|
|
|
|
+| 数据关系 | 验证父子约束 | 验证父子关联 |
|
|
|
|
|
+| 操作序列 | 先子后父删除 | 从上到下创建 |
|
|
|
|
|
+| 树形操作 | 删除节点 | 展开节点验证 |
|
|
|
|
|
+| 测试复杂度 | 中等 | 较高(多层级) |
|
|
|
|
|
+
|
|
|
|
|
+### 项目结构说明
|
|
|
|
|
+
|
|
|
|
|
+**目标文件位置:**
|
|
|
|
|
+```
|
|
|
|
|
+web/tests/e2e/specs/admin/region-cascade.spec.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**导入路径:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+import { test, expect } from '../../utils/test-setup';
|
|
|
|
|
+// test-setup 包含:
|
|
|
|
|
+// - adminLoginPage fixture
|
|
|
|
|
+// - regionManagementPage fixture
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**测试命令:**
|
|
|
|
|
+```bash
|
|
|
|
|
+# 运行级联选择测试
|
|
|
|
|
+cd web
|
|
|
|
|
+pnpm test:e2e:chromium region-cascade
|
|
|
|
|
+
|
|
|
|
|
+# 快速失败模式(调试)
|
|
|
|
|
+timeout 60 pnpm test:e2e:chromium region-cascade
|
|
|
|
|
+
|
|
|
|
|
+# 运行所有区域管理测试
|
|
|
|
|
+pnpm test:e2e:chromium region-*.spec.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### TypeScript + Playwright 陷阱预防
|
|
|
|
|
+
|
|
|
|
|
+⚠️ **树形结构异步加载**
|
|
|
|
|
+- 创建子区域后,树形结构需要时间刷新
|
|
|
|
|
+- 必须等待 `waitForTreeLoaded()` 验证更新完成
|
|
|
|
|
+- 展开节点后,子节点可能需要额外加载时间
|
|
|
|
|
+
|
|
|
|
|
+✅ **正确做法:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 创建子区域后等待树形结构刷新
|
|
|
|
|
+await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+
|
|
|
|
|
+// 展开节点后等待子节点可见
|
|
|
|
|
+await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+const exists = await regionManagementPage.regionExists(cityName);
|
|
|
|
|
+expect(exists).toBe(true);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+❌ **避免:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 避免创建后立即验证,树形结构可能未更新
|
|
|
|
|
+await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+const exists = await regionManagementPage.regionExists(cityName); // 可能失败
|
|
|
|
|
+
|
|
|
|
|
+// 避免展开后立即验证子节点
|
|
|
|
|
+await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+const exists = await regionManagementPage.regionExists(cityName); // 可能未加载
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 测试调试技巧
|
|
|
|
|
+
|
|
|
|
|
+**1. 查看树形结构:**
|
|
|
|
|
+```bash
|
|
|
|
|
+# 使用 Playwright Inspector
|
|
|
|
|
+cd web
|
|
|
|
|
+pnpm test:e2e:chromium region-cascade --debug
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**2. 查看错误上下文:**
|
|
|
|
|
+```bash
|
|
|
|
|
+# 测试失败后查看
|
|
|
|
|
+cat test-results/*/error-context.md
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**3. 添加调试输出:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+test('调试级联选择', async ({ regionManagementPage }) => {
|
|
|
|
|
+ const provinceName = '测试省';
|
|
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
|
|
+ console.debug('省份已创建:', provinceName);
|
|
|
|
|
+
|
|
|
|
|
+ await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+ console.debug('省份已展开');
|
|
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+ console.debug('树形结构已刷新');
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 测试覆盖率目标
|
|
|
|
|
+
|
|
|
|
|
+**本 Story 的测试覆盖率:**
|
|
|
|
|
+- 完整三级级联选择(省市区): 100%
|
|
|
|
|
+- 多个子区域级联: 100%
|
|
|
|
|
+- 级联编辑场景: 100%
|
|
|
|
|
+- 深层级级联: 100%
|
|
|
|
|
+
|
|
|
|
|
+**测试通过率目标:** 连续运行 10 次,100% 通过
|
|
|
|
|
+
|
|
|
|
|
+### 后续 Story 依赖
|
|
|
|
|
+
|
|
|
|
|
+本测试完成后,后续 Story 依赖:
|
|
|
|
|
+- Story 8.7: 运行测试并收集问题和改进建议
|
|
|
|
|
+- Story 8.8: 扩展工具包(如需要,评估后决定)
|
|
|
|
|
+
|
|
|
|
|
+## Project Structure Notes
|
|
|
|
|
+
|
|
|
|
|
+### 对齐统一项目结构
|
|
|
|
|
+
|
|
|
|
|
+**目标文件位置:**
|
|
|
|
|
+```
|
|
|
|
|
+web/tests/e2e/specs/admin/region-cascade.spec.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**遵循模式:**
|
|
|
|
|
+- 测试文件: `web/tests/e2e/specs/admin/*.spec.ts`
|
|
|
|
|
+- Page Object: `web/tests/e2e/pages/admin/*.page.ts`
|
|
|
|
|
+- Fixtures: `web/tests/e2e/fixtures/*.json`
|
|
|
|
|
+
|
|
|
|
|
+**与现有测试对齐:**
|
|
|
|
|
+- 使用 `test.describe.serial()` 组织测试组
|
|
|
|
|
+- 使用 `beforeEach`/`afterEach` 处理测试设置和清理
|
|
|
|
|
+- 使用 fixtures 从 `test-setup.ts` 导入
|
|
|
|
|
+- 使用 `generateUniqueRegionName()` 生成唯一测试数据
|
|
|
|
|
+
|
|
|
|
|
+## References
|
|
|
|
|
+
|
|
|
|
|
+**源文档和规范:**
|
|
|
|
|
+- [Source: `_bmad-output/planning-artifacts/epics.md`](Epic 8 - 区域管理 E2E 测试)
|
|
|
|
|
+- [Source: `_bmad-output/planning-artifacts/architecture.md`](测试架构和标准)
|
|
|
|
|
+- [Source: `_bmad-output/project-context.md`](项目上下文和规则)
|
|
|
|
|
+
|
|
|
|
|
+**前置 Story 参考:**
|
|
|
|
|
+- [Source: `_bmad-output/implementation-artifacts/8-1-region-page-object.md`](RegionManagementPage 实现)
|
|
|
|
|
+- [Source: `_bmad-output/implementation-artifacts/8-3-add-region-test.md`](添加区域测试实现)
|
|
|
|
|
+- [Source: `_bmad-output/implementation-artifacts/8-4-edit-region-test.md`](编辑区域测试实现)
|
|
|
|
|
+- [Source: `_bmad-output/implementation-artifacts/8-5-delete-region-test.md`](删除区域测试实现)
|
|
|
|
|
+
|
|
|
|
|
+**代码参考:**
|
|
|
|
|
+- [Source: `web/tests/e2e/pages/admin/region-management.page.ts`](Page Object 实现)
|
|
|
|
|
+- [Source: `web/tests/e2e/specs/admin/region-add.spec.ts`](添加区域测试)
|
|
|
|
|
+- [Source: `web/tests/e2e/utils/test-setup.ts`](Test fixtures)
|
|
|
|
|
+
|
|
|
|
|
+## Dev Agent Record
|
|
|
|
|
+
|
|
|
|
|
+### Agent Model Used
|
|
|
|
|
+
|
|
|
|
|
+- Model: Claude (Sonnet)
|
|
|
|
|
+- Date: 2026-01-11
|
|
|
|
|
+
|
|
|
|
|
+### Debug Log References
|
|
|
|
|
+
|
|
|
|
|
+无调试问题(Story 创建阶段)
|
|
|
|
|
+
|
|
|
|
|
+### Completion Notes List
|
|
|
|
|
+
|
|
|
|
|
+**Story 创建完成 (2026-01-11):**
|
|
|
|
|
+- ✅ 创建了 `_bmad-output/implementation-artifacts/8-6-cascade-select-test.md` Story 文档
|
|
|
|
|
+- ✅ 分析了级联选择的 UI 设计模式(树形结构 vs 下拉框)
|
|
|
|
|
+- ✅ 提供了完整的测试用例设计模板
|
|
|
|
|
+- ✅ 包含测试覆盖率目标和调试技巧
|
|
|
|
|
+
|
|
|
|
|
+**关键设计决策:**
|
|
|
|
|
+1. **UI 模式澄清**: 当前区域管理使用树形结构设计,而非传统的下拉框级联选择
|
|
|
|
|
+2. **测试策略调整**: "级联选择"测试转化为"逐级创建并验证树形结构"的测试场景
|
|
|
|
|
+3. **等待策略**: 强调 `waitForTreeLoaded()` 在创建子区域和展开节点后的使用
|
|
|
|
|
+
|
|
|
|
|
+### File List
|
|
|
|
|
+
|
|
|
|
|
+**Story 文档:**
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/8-6-cascade-select-test.md` (本文件)
|
|
|
|
|
+
|
|
|
|
|
+**待创建文件:**
|
|
|
|
|
+- `web/tests/e2e/specs/admin/region-cascade.spec.ts` (测试文件 - 由 dev-story 创建)
|
|
|
|
|
+
|
|
|
|
|
+**参考文件 (只读):**
|
|
|
|
|
+- `web/tests/e2e/pages/admin/region-management.page.ts`
|
|
|
|
|
+- `web/tests/e2e/specs/admin/region-add.spec.ts`
|
|
|
|
|
+- `web/tests/e2e/utils/test-setup.ts`
|
|
|
|
|
+
|
|
|
|
|
+## Project Context Reference
|
|
|
|
|
+
|
|
|
|
|
+### 关键项目规则摘要
|
|
|
|
|
+
|
|
|
|
|
+**技术栈:**
|
|
|
|
|
+- Playwright 1.55.0 - E2E 测试框架
|
|
|
|
|
+- TypeScript 5.9.3 - 严格模式
|
|
|
|
|
+- @d8d/e2e-test-utils - 内部测试工具包(本 Story 不需要,因为使用树形结构)
|
|
|
|
|
+- Node.js 20.19.2
|
|
|
|
|
+- pnpm 10.18.3 - 包管理
|
|
|
|
|
+
|
|
|
|
|
+**测试命令:**
|
|
|
|
|
+```bash
|
|
|
|
|
+# 运行级联选择测试
|
|
|
|
|
+cd web
|
|
|
|
|
+pnpm test:e2e:chromium region-cascade
|
|
|
|
|
+
|
|
|
|
|
+# 快速失败模式(调试)
|
|
|
|
|
+timeout 60 pnpm test:e2e:chromium region-cascade
|
|
|
|
|
+
|
|
|
|
|
+# 运行所有区域管理测试
|
|
|
|
|
+pnpm test:e2e:chromium region-*.spec.ts
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**命名约定:**
|
|
|
|
|
+- 测试文件名: kebab-case + `.spec.ts` 后缀
|
|
|
|
|
+- 测试组: 使用 `test.describe.serial()` 分组
|
|
|
|
|
+- 测试名称: 中文描述,格式 "应该..."
|
|
|
|
|
+
|
|
|
|
|
+### 必须遵循的架构决策
|
|
|
|
|
+
|
|
|
|
|
+**来自 Architecture.md 的关键决策:**
|
|
|
|
|
+
|
|
|
|
|
+1. **选择器策略(混合策略优先级):**
|
|
|
|
|
+ - `data-testid` - 最高优先级
|
|
|
|
|
+ - `aria-label` + role - 无障碍标准
|
|
|
|
|
+ - Text content + role - 兜底方案
|
|
|
|
|
+
|
|
|
|
|
+2. **树形结构特殊处理:**
|
|
|
|
|
+ - 异步加载:树形结构的子节点是懒加载的
|
|
|
|
|
+ - 等待策略:每次操作后必须等待 `waitForTreeLoaded()`
|
|
|
|
|
+ - 展开节点:展开后子节点可能需要额外加载时间
|
|
|
|
|
+
|
|
|
|
|
+3. **测试隔离:**
|
|
|
|
|
+ - 每个测试使用独立数据
|
|
|
|
|
+ - 测试后清理数据
|
|
|
|
|
+ - 支持并行执行(使用 `test.describe.serial` 时串行)
|
|
|
|
|
+
|
|
|
|
|
+4. **TypeScript 严格模式:**
|
|
|
|
|
+ - 所有变量必须有明确类型
|
|
|
|
|
+ - 禁止使用 `any` 类型
|
|
|
|
|
+ - 使用 `import` 配合 `vi.mocked`(Vitest)
|
|
|
|
|
+
|
|
|
|
|
+### TypeScript + Playwright 陷阱预防
|
|
|
|
|
+
|
|
|
|
|
+**来自 Architecture.md "TypeScript + Playwright 常见陷阱" 部分:**
|
|
|
|
|
+
|
|
|
|
|
+⚠️ **树形结构异步加载必须处理**
|
|
|
|
|
+- Story 8.1 已验证树形结构 DOM
|
|
|
|
|
+- 使用 RegionManagementPage 的封装方法
|
|
|
|
|
+- 创建子区域后必须等待刷新
|
|
|
|
|
+
|
|
|
|
|
+✅ **正确做法:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 每次创建子区域后等待
|
|
|
|
|
+await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+
|
|
|
|
|
+// 展开节点后等待子节点加载
|
|
|
|
|
+await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+await regionManagementPage.waitForTreeLoaded();
|
|
|
|
|
+const exists = await regionManagementPage.regionExists(cityName);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+❌ **避免:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 避免假设树形结构立即更新
|
|
|
|
|
+await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
|
|
+const exists = await regionManagementPage.regionExists(cityName); // 可能失败
|
|
|
|
|
+
|
|
|
|
|
+// 避免假设展开立即显示子节点
|
|
|
|
|
+await regionManagementPage.expandNode(provinceName);
|
|
|
|
|
+const exists = await regionManagementPage.regionExists(cityName); // 可能未加载
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 代码质量检查清单
|
|
|
|
|
+
|
|
|
|
|
+**代码质量:**
|
|
|
|
|
+- [ ] 测试用例有清晰的描述
|
|
|
|
|
+- [ ] 使用 `test.describe.serial()` 组织相关测试
|
|
|
|
|
+- [ ] 每个测试独立运行,不依赖其他测试
|
|
|
|
|
+
|
|
|
|
|
+**测试数据:**
|
|
|
|
|
+- [ ] 使用唯一标识符避免数据冲突
|
|
|
|
|
+- [ ] 测试后清理测试数据
|
|
|
|
|
+- [ ] 使用 `beforeEach`/`afterEach` 钩子
|
|
|
|
|
+
|
|
|
|
|
+**树形操作:**
|
|
|
|
|
+- [ ] 创建子区域后调用 `waitForTreeLoaded()`
|
|
|
|
|
+- [ ] 展开节点后调用 `waitForTreeLoaded()`
|
|
|
|
|
+- [ ] 验证时考虑节点可能需要滚动到可视区域
|
|
|
|
|
+
|
|
|
|
|
+### 参考文档位置
|
|
|
|
|
+
|
|
|
|
|
+| 文档 | 路径 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| 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` |
|
|
|
|
|
+| Story 8.1 | `_bmad-output/implementation-artifacts/8-1-region-page-object.md` |
|
|
|
|
|
+| Story 8.3 | `_bmad-output/implementation-artifacts/8-3-add-region-test.md` |
|
|
|
|
|
+| Story 8.4 | `_bmad-output/implementation-artifacts/8-4-edit-region-test.md` |
|
|
|
|
|
+| Story 8.5 | `_bmad-output/implementation-artifacts/8-5-delete-region-test.md` |
|
|
|
|
|
+| RegionManagementPage | `web/tests/e2e/pages/admin/region-management.page.ts` |
|
|
|
|
|
+| 参考测试 | `web/tests/e2e/specs/admin/region-add.spec.ts` |
|
|
|
|
|
+| test-setup | `web/tests/e2e/utils/test-setup.ts` |
|
|
|
|
|
+
|
|
|
|
|
+### 相关 Epic 和 Story
|
|
|
|
|
+
|
|
|
|
|
+**前置 Epic:**
|
|
|
|
|
+- Epic 1: ✅ 完成 - Select 工具基础框架
|
|
|
|
|
+- Epic 2: ✅ 完成 - Select 工具在真实 E2E 测试中验证
|
|
|
|
|
+
|
|
|
|
|
+**当前 Epic (Epic 8):**
|
|
|
|
|
+- Story 8.1: ✅ 完成 - 创建区域管理 Page Object
|
|
|
|
|
+- Story 8.2: ✅ 完成 - 编写区域列表查看测试
|
|
|
|
|
+- Story 8.3: ✅ 完成 - 编写添加区域测试
|
|
|
|
|
+- Story 8.4: ✅ 完成 - 编写编辑区域测试
|
|
|
|
|
+- Story 8.5: ✅ 完成 - 编写删除区域测试
|
|
|
|
|
+- Story 8.6: 📝 当前 - 编写级联选择完整流程测试
|
|
|
|
|
+- Story 8.7: ⏳ 待开始 - 运行测试并收集问题和改进建议
|
|
|
|
|
+
|
|
|
|
|
+**后续 Epic:**
|
|
|
|
|
+- Epic 9: 🔄 进行中 - 残疾人管理完整 E2E 测试覆盖
|
|
|
|
|
+- Epic 10: 🔄 进行中 - 订单管理 E2E 测试
|
|
|
|
|
+
|
|
|
|
|
+## Completion Status
|
|
|
|
|
+
|
|
|
|
|
+**Story ID:** 8.6
|
|
|
|
|
+**Story Key:** 8-6-cascade-select-test
|
|
|
|
|
+**Epic:** Epic 8 - 区域管理 E2E 测试 (Epic B)
|
|
|
|
|
+**Status:** ready-for-dev
|
|
|
|
|
+
|
|
|
|
|
+**交付物:**
|
|
|
|
|
+- [x] Story 文档创建完成
|
|
|
|
|
+- [ ] 级联选择完整流程测试实现(待 dev-story)
|
|
|
|
|
+- [ ] 测试在真实浏览器中通过(待 dev-story)
|
|
|
|
|
+
|
|
|
|
|
+**实现摘要:**
|
|
|
|
|
+- 创建了 `_bmad-output/implementation-artifacts/8-6-cascade-select-test.md` Story 文档
|
|
|
|
|
+- 澄清了级联选择的 UI 设计模式(树形结构 vs 下拉框)
|
|
|
|
|
+- 提供了完整的测试用例设计模板:
|
|
|
|
|
+ - 完整三级级联选择测试(省→市→区)
|
|
|
|
|
+ - 多个子区域级联测试
|
|
|
|
|
+ - 级联编辑场景测试
|
|
|
|
|
+ - 深层级级联测试
|
|
|
|
|
+
|
|
|
|
|
+**下一步操作:**
|
|
|
|
|
+1. ➡️ 执行 dev-story 实现 Story 8.6
|
|
|
|
|
+2. ➡️ 完成后进入 Story 8.7(运行测试并收集问题)
|