|
|
@@ -0,0 +1,762 @@
|
|
|
+# Story 8.4: 编写编辑区域测试
|
|
|
+
|
|
|
+Status: ready-for-dev
|
|
|
+
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
+
|
|
|
+## Story
|
|
|
+
|
|
|
+作为测试开发者,
|
|
|
+我想要编写编辑区域的 E2E 测试,
|
|
|
+以便验证区域信息的修改功能。
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+
|
|
|
+**Given** 添加区域测试已通过
|
|
|
+**When** 编写编辑区域测试用例
|
|
|
+**Then** 验证编辑区域名称的流程
|
|
|
+**And** 验证修改区域状态的流程(如启用/禁用)
|
|
|
+**And** 验证编辑后列表中正确显示更新后的信息
|
|
|
+**And** 验证必填字段的验证规则
|
|
|
+**And** 测试在真实浏览器中通过
|
|
|
+
|
|
|
+## Tasks / Subtasks
|
|
|
+
|
|
|
+- [ ] 创建测试文件基础结构 (AC: #)
|
|
|
+ - [ ] 创建 `web/tests/e2e/specs/admin/region-edit.spec.ts`
|
|
|
+ - [ ] 配置 test fixtures(adminLoginPage, regionManagementPage)
|
|
|
+ - [ ] 设置测试组和 beforeEach/afterEach 钩子
|
|
|
+- [ ] 实现编辑区域名称测试 (AC: 1, 4, 5)
|
|
|
+ - [ ] 测试打开编辑对话框
|
|
|
+ - [ ] 测试修改区域名称
|
|
|
+ - [ ] 验证编辑后列表中正确显示更新后的名称
|
|
|
+- [ ] 实现修改区域状态测试 (AC: 2, 4, 5)
|
|
|
+ - [ ] 测试启用已禁用的区域
|
|
|
+ - [ ] 测试禁用已启用的区域
|
|
|
+ - [ ] 验证状态切换后列表中正确显示新状态
|
|
|
+- [ ] 实现编辑区域代码测试 (AC: 1, 5)
|
|
|
+ - [ ] 测试修改行政区划代码
|
|
|
+ - [ ] 验证代码更新成功
|
|
|
+- [ ] 实现表单验证测试 (AC: 5)
|
|
|
+ - [ ] 测试清空名称时的错误提示
|
|
|
+ - [ ] 测试修改为已存在名称的处理
|
|
|
+- [ ] 实现测试数据隔离 (AC: #)
|
|
|
+ - [ ] 每个测试使用唯一的区域名称
|
|
|
+ - [ ] 测试后清理测试数据
|
|
|
+
|
|
|
+## 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: ✅ 已完成 - 添加区域测试
|
|
|
+
|
|
|
+**依赖:**
|
|
|
+- Epic 1: ✅ 已完成(Select 工具基础框架)
|
|
|
+- Epic 2: ✅ 已完成(Select 工具在真实 E2E 测试中验证)
|
|
|
+- Epic 3: ✅ 已完成(文件上传工具、级联选择工具)
|
|
|
+- Story 8.1: ✅ 已完成(RegionManagementPage Page Object)
|
|
|
+- Story 8.3: ✅ 已完成(添加区域测试)
|
|
|
+
|
|
|
+### 区域编辑功能概述
|
|
|
+
|
|
|
+区域管理支持两种编辑操作:
|
|
|
+1. **编辑区域信息** - 修改区域名称、行政区划代码
|
|
|
+2. **切换区域状态** - 启用/禁用区域
|
|
|
+
|
|
|
+**编辑对话框字段(基于 AreaForm.tsx):**
|
|
|
+- 区域名称: 文本输入框(必填)
|
|
|
+- 区域代码: 文本输入框(可选)
|
|
|
+
|
|
|
+**状态切换功能:**
|
|
|
+- 启用状态 → 禁用状态
|
|
|
+- 禁用状态 → 启用状态
|
|
|
+
|
|
|
+### RegionManagementPage API 参考
|
|
|
+
|
|
|
+**编辑区域相关方法(来自 Story 8.1):**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 打开编辑区域对话框
|
|
|
+await regionManagementPage.openEditDialog('区域名称');
|
|
|
+
|
|
|
+// 编辑区域信息
|
|
|
+const result = await regionManagementPage.editRegion('原区域名称', {
|
|
|
+ name: '新区域名称',
|
|
|
+ code: 'NEW_CODE'
|
|
|
+});
|
|
|
+// result.success: boolean
|
|
|
+// result.hasSuccess: boolean
|
|
|
+// result.hasError: boolean
|
|
|
+
|
|
|
+// 获取区域状态
|
|
|
+const status = await regionManagementPage.getRegionStatus('区域名称');
|
|
|
+// 返回: '启用' | '禁用' | null
|
|
|
+
|
|
|
+// 打开状态切换对话框
|
|
|
+await regionManagementPage.openToggleStatusDialog('区域名称');
|
|
|
+
|
|
|
+// 确认状态切换
|
|
|
+await regionManagementPage.confirmToggleStatus();
|
|
|
+
|
|
|
+// 取消状态切换
|
|
|
+await regionManagementPage.cancelToggleStatus();
|
|
|
+
|
|
|
+// 快捷方法:切换区域状态
|
|
|
+const success = await regionManagementPage.toggleRegionStatus('区域名称');
|
|
|
+// 返回: boolean(true = 成功, false = 失败)
|
|
|
+```
|
|
|
+
|
|
|
+**选择器策略(来自 Story 8.1):**
|
|
|
+- 编辑按钮: `getByRole('button', { name: '编辑' })`
|
|
|
+- 状态切换按钮: `getByRole('button', { name: '启用' | '禁用' })`
|
|
|
+- 对话框: `[role="dialog"]`
|
|
|
+- 表单字段标签: 使用精确文本匹配 `getByLabel('区域名称')`
|
|
|
+- Toast 消息: `[data-sonner-toast][data-type="success|error"]`
|
|
|
+- 状态 Badge: `.badge` + `text='启用'|'禁用'`
|
|
|
+
|
|
|
+### 测试文件结构模式
|
|
|
+
|
|
|
+参考 `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'));
|
|
|
+
|
|
|
+test.describe.serial('编辑区域测试', () => {
|
|
|
+ let createdProvinceName: string;
|
|
|
+
|
|
|
+ test.beforeEach(async ({ adminLoginPage, regionManagementPage }) => {
|
|
|
+ // 登录
|
|
|
+ await adminLoginPage.goto();
|
|
|
+ await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
|
|
|
+ await adminLoginPage.expectLoginSuccess();
|
|
|
+
|
|
|
+ // 导航到区域管理页面
|
|
|
+ await regionManagementPage.goto();
|
|
|
+ });
|
|
|
+
|
|
|
+ test.afterEach(async ({ regionManagementPage }) => {
|
|
|
+ // 清理测试数据
|
|
|
+ if (createdProvinceName) {
|
|
|
+ try {
|
|
|
+ await regionManagementPage.deleteRegion(createdProvinceName);
|
|
|
+ } catch (error) {
|
|
|
+ console.debug('清理测试数据失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该成功编辑区域名称', async ({ regionManagementPage }) => {
|
|
|
+ // 测试实现
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 测试用例设计
|
|
|
+
|
|
|
+**1. 编辑区域名称测试:**
|
|
|
+```typescript
|
|
|
+test.describe('编辑区域名称', () => {
|
|
|
+ test('应该成功编辑区域名称', async ({ regionManagementPage }) => {
|
|
|
+ // 首先创建一个测试省份
|
|
|
+ const originalName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({ name: originalName });
|
|
|
+
|
|
|
+ // 编辑区域名称
|
|
|
+ const newName = `编辑后的省_${Date.now()}`;
|
|
|
+ const result = await regionManagementPage.editRegion(originalName, {
|
|
|
+ name: newName
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证编辑成功
|
|
|
+ expect(result.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证列表中显示新名称
|
|
|
+ await regionManagementPage.waitForTreeLoaded();
|
|
|
+ const exists = await regionManagementPage.regionExists(newName);
|
|
|
+ expect(exists).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('编辑后原名称不应存在', async ({ regionManagementPage }) => {
|
|
|
+ const originalName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({ name: originalName });
|
|
|
+
|
|
|
+ const newName = `编辑后的省_${Date.now()}`;
|
|
|
+ await regionManagementPage.editRegion(originalName, { name: newName });
|
|
|
+
|
|
|
+ // 验证原名称不存在
|
|
|
+ const originalExists = await regionManagementPage.regionExists(originalName);
|
|
|
+ expect(originalExists).toBe(false);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+**2. 修改区域代码测试:**
|
|
|
+```typescript
|
|
|
+test.describe('修改区域代码', () => {
|
|
|
+ test('应该成功修改行政区划代码', async ({ regionManagementPage }) => {
|
|
|
+ const provinceName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({
|
|
|
+ name: provinceName,
|
|
|
+ code: 'OLD_CODE'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 修改代码
|
|
|
+ const newCode = `NEW_${Date.now()}`;
|
|
|
+ const result = await regionManagementPage.editRegion(provinceName, {
|
|
|
+ code: newCode
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(result.success).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该能同时修改名称和代码', async ({ regionManagementPage }) => {
|
|
|
+ const originalName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({
|
|
|
+ name: originalName,
|
|
|
+ code: 'OLD_CODE'
|
|
|
+ });
|
|
|
+
|
|
|
+ const newName = `新省名_${Date.now()}`;
|
|
|
+ const newCode = `NEW_${Date.now()}`;
|
|
|
+ const result = await regionManagementPage.editRegion(originalName, {
|
|
|
+ name: newName,
|
|
|
+ code: newCode
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(result.success).toBe(true);
|
|
|
+ expect(await regionManagementPage.regionExists(newName)).toBe(true);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+**3. 状态切换测试:**
|
|
|
+```typescript
|
|
|
+test.describe('区域状态切换', () => {
|
|
|
+ test('应该成功禁用已启用的区域', async ({ regionManagementPage }) => {
|
|
|
+ const provinceName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
+
|
|
|
+ // 获取初始状态
|
|
|
+ const initialStatus = await regionManagementPage.getRegionStatus(provinceName);
|
|
|
+ expect(initialStatus).toBe('启用');
|
|
|
+
|
|
|
+ // 禁用区域
|
|
|
+ const success = await regionManagementPage.toggleRegionStatus(provinceName);
|
|
|
+ expect(success).toBe(true);
|
|
|
+
|
|
|
+ // 验证状态已更新
|
|
|
+ await regionManagementPage.page.waitForTimeout(1000);
|
|
|
+ const newStatus = await regionManagementPage.getRegionStatus(provinceName);
|
|
|
+ expect(newStatus).toBe('禁用');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该成功启用已禁用的区域', async ({ regionManagementPage }) => {
|
|
|
+ const provinceName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
+
|
|
|
+ // 先禁用
|
|
|
+ await regionManagementPage.toggleRegionStatus(provinceName);
|
|
|
+
|
|
|
+ // 再启用
|
|
|
+ const success = await regionManagementPage.toggleRegionStatus(provinceName);
|
|
|
+ expect(success).toBe(true);
|
|
|
+
|
|
|
+ // 验证状态已恢复为启用
|
|
|
+ await regionManagementPage.page.waitForTimeout(1000);
|
|
|
+ const status = await regionManagementPage.getRegionStatus(provinceName);
|
|
|
+ expect(status).toBe('启用');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('取消状态切换应保持原状态', async ({ regionManagementPage }) => {
|
|
|
+ const provinceName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
+
|
|
|
+ const initialStatus = await regionManagementPage.getRegionStatus(provinceName);
|
|
|
+
|
|
|
+ // 打开状态切换对话框但取消
|
|
|
+ await regionManagementPage.openToggleStatusDialog(provinceName);
|
|
|
+ await regionManagementPage.cancelToggleStatus();
|
|
|
+
|
|
|
+ // 验证状态未改变
|
|
|
+ await regionManagementPage.page.waitForTimeout(500);
|
|
|
+ const currentStatus = await regionManagementPage.getRegionStatus(provinceName);
|
|
|
+ expect(currentStatus).toBe(initialStatus);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+**4. 表单验证测试:**
|
|
|
+```typescript
|
|
|
+test.describe('表单验证', () => {
|
|
|
+ test('清空名称时应显示错误提示', async ({ regionManagementPage }) => {
|
|
|
+ const provinceName = `测试省_${Date.now()}`;
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
+
|
|
|
+ // 打开编辑对话框并清空名称
|
|
|
+ await regionManagementPage.openEditDialog(provinceName);
|
|
|
+ await regionManagementPage.page.getByLabel('区域名称').fill('');
|
|
|
+
|
|
|
+ // 提交表单
|
|
|
+ const submitButton = regionManagementPage.page.getByRole('button', { name: '更新' });
|
|
|
+ await submitButton.click();
|
|
|
+
|
|
|
+ // 验证错误提示
|
|
|
+ await expect(regionManagementPage.page.getByText('区域名称不能为空'))
|
|
|
+ .toBeVisible();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('修改为已存在名称应显示错误提示', async ({ regionManagementPage }) => {
|
|
|
+ const province1 = `测试省1_${Date.now()}`;
|
|
|
+ const province2 = `测试省2_${Date.now()}`;
|
|
|
+
|
|
|
+ // 创建两个省份
|
|
|
+ await regionManagementPage.createProvince({ name: province1 });
|
|
|
+ await regionManagementPage.createProvince({ name: province2 });
|
|
|
+
|
|
|
+ // 尝试将 province2 改为 province1 的名称
|
|
|
+ const result = await regionManagementPage.editRegion(province2, {
|
|
|
+ name: province1
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证编辑失败
|
|
|
+ expect(result.success).toBe(false);
|
|
|
+ expect(result.hasError).toBe(true);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+**5. 编辑子区域测试:**
|
|
|
+```typescript
|
|
|
+test.describe('编辑子区域', () => {
|
|
|
+ test('应该成功编辑市级区域名称', async ({ regionManagementPage }) => {
|
|
|
+ const provinceName = `测试省_${Date.now()}`;
|
|
|
+ const originalCityName = `测试市_${Date.now()}`;
|
|
|
+
|
|
|
+ // 创建省和市
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', {
|
|
|
+ name: originalCityName
|
|
|
+ });
|
|
|
+
|
|
|
+ // 编辑城市名称
|
|
|
+ const newCityName = `编辑后的市_${Date.now()}`;
|
|
|
+ const result = await regionManagementPage.editRegion(originalCityName, {
|
|
|
+ name: newCityName
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(result.success).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该成功编辑区级区域状态', async ({ regionManagementPage }) => {
|
|
|
+ const provinceName = `测试省_${Date.now()}`;
|
|
|
+ const cityName = `测试市_${Date.now()}`;
|
|
|
+ const districtName = `测试区_${Date.now()}`;
|
|
|
+
|
|
|
+ // 创建省市区三级结构
|
|
|
+ await regionManagementPage.createProvince({ name: provinceName });
|
|
|
+ await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
|
|
|
+ await regionManagementPage.createChildRegion(cityName, '区', { name: districtName });
|
|
|
+
|
|
|
+ // 切换区的状态
|
|
|
+ const success = await regionManagementPage.toggleRegionStatus(districtName);
|
|
|
+ expect(success).toBe(true);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 测试数据管理策略
|
|
|
+
|
|
|
+**数据生成工具:**
|
|
|
+```typescript
|
|
|
+/**
|
|
|
+ * 生成唯一区域名称
|
|
|
+ */
|
|
|
+function generateUniqueRegionName(prefix: string = '测试区域'): string {
|
|
|
+ const timestamp = Date.now();
|
|
|
+ const random = Math.floor(Math.random() * 1000);
|
|
|
+ return `${prefix}_${timestamp}_${random}`;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 生成唯一区域代码
|
|
|
+ */
|
|
|
+function generateUniqueRegionCode(level: string): string {
|
|
|
+ const timestamp = Date.now();
|
|
|
+ return `${level.toUpperCase()}_${timestamp}`;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**数据清理策略:**
|
|
|
+- 使用 `test.afterEach` 清理每个测试创建的数据
|
|
|
+- 使用 `try-catch` 处理清理失败的情况
|
|
|
+- 记录清理失败的日志
|
|
|
+
|
|
|
+```typescript
|
|
|
+test.afterEach(async ({ regionManagementPage }) => {
|
|
|
+ if (createdProvinceName) {
|
|
|
+ try {
|
|
|
+ await regionManagementPage.deleteRegion(createdProvinceName);
|
|
|
+ createdProvinceName = '';
|
|
|
+ } catch (error) {
|
|
|
+ console.debug('清理测试数据失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 与前序 Story 的关键差异
|
|
|
+
|
|
|
+| 方面 | Story 8.3(添加区域) | Story 8.4(编辑区域) |
|
|
|
+|------|---------------------|---------------------|
|
|
|
+| 主要操作 | 创建新数据 | 修改现有数据 |
|
|
|
+| 前置条件 | 无 | 需要先创建测试数据 |
|
|
|
+| 对话框类型 | 创建对话框 | 编辑对话框 |
|
|
|
+| 表单状态 | 空表单 | 预填充现有数据 |
|
|
|
+| 验证重点 | 创建成功、级联选择 | 数据更新、状态切换 |
|
|
|
+| 数据清理 | 创建后可删除 | 编辑后仍需删除 |
|
|
|
+
|
|
|
+### 项目结构说明
|
|
|
+
|
|
|
+**目标文件位置:**
|
|
|
+```
|
|
|
+web/tests/e2e/specs/admin/region-edit.spec.ts
|
|
|
+```
|
|
|
+
|
|
|
+**导入路径:**
|
|
|
+```typescript
|
|
|
+import { test, expect } from '../../utils/test-setup';
|
|
|
+// test-setup 包含:
|
|
|
+// - adminLoginPage fixture
|
|
|
+// - regionManagementPage fixture
|
|
|
+```
|
|
|
+
|
|
|
+**测试命令:**
|
|
|
+```bash
|
|
|
+# 运行编辑区域测试
|
|
|
+cd web
|
|
|
+pnpm test:e2e:chromium region-edit
|
|
|
+
|
|
|
+# 快速失败模式(调试)
|
|
|
+timeout 60 pnpm test:e2e:chromium region-edit
|
|
|
+
|
|
|
+# 运行所有区域管理测试
|
|
|
+pnpm test:e2e:chromium region-*.spec.ts
|
|
|
+```
|
|
|
+
|
|
|
+### TypeScript + Playwright 陷阱预防
|
|
|
+
|
|
|
+⚠️ **DOM 结构假设必须验证**
|
|
|
+- Story 8.1 已验证编辑对话框结构
|
|
|
+- 使用已验证的选择器策略
|
|
|
+
|
|
|
+✅ **正确做法:**
|
|
|
+```typescript
|
|
|
+// 使用 RegionManagementPage 的封装方法
|
|
|
+await regionManagementPage.openEditDialog('区域名称');
|
|
|
+await regionManagementPage.fillRegionForm({ name: '新名称' });
|
|
|
+await regionManagementPage.submitForm();
|
|
|
+
|
|
|
+// 使用精确文本匹配验证
|
|
|
+await expect(page.getByText('更新成功', { exact: true })).toBeVisible();
|
|
|
+```
|
|
|
+
|
|
|
+❌ **避免:**
|
|
|
+```typescript
|
|
|
+// 避免直接操作 DOM
|
|
|
+await page.locator('.dialog').click();
|
|
|
+await page.fill('input[name="name"]', '新名称');
|
|
|
+
|
|
|
+// 避免假设状态立即更新
|
|
|
+const status = await page.getRegionStatus('区域名称');
|
|
|
+expect(status).toBe('禁用'); // 可能需要等待
|
|
|
+```
|
|
|
+
|
|
|
+### 测试调试技巧
|
|
|
+
|
|
|
+**1. 查看 DOM 结构:**
|
|
|
+```bash
|
|
|
+# 使用 Playwright Inspector
|
|
|
+cd web
|
|
|
+pnpm test:e2e:chromium region-edit --debug
|
|
|
+```
|
|
|
+
|
|
|
+**2. 查看错误上下文:**
|
|
|
+```bash
|
|
|
+# 测试失败后查看
|
|
|
+cat test-results/*/error-context.md
|
|
|
+```
|
|
|
+
|
|
|
+**3. 添加调试输出:**
|
|
|
+```typescript
|
|
|
+test('调试测试', async ({ regionManagementPage }) => {
|
|
|
+ const result = await regionManagementPage.editRegion('旧名称', { name: '新名称' });
|
|
|
+ console.debug('编辑结果:', result);
|
|
|
+ console.debug('成功消息:', result.successMessage);
|
|
|
+ console.debug('错误消息:', result.errorMessage);
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 测试覆盖率目标
|
|
|
+
|
|
|
+**本 Story 的测试覆盖率:**
|
|
|
+- 编辑区域名称: 100%
|
|
|
+- 修改区域代码: 100%
|
|
|
+- 状态切换(启用→禁用): 100%
|
|
|
+- 状态切换(禁用→启用): 100%
|
|
|
+- 表单验证: 100%
|
|
|
+- 编辑子区域: 100%
|
|
|
+
|
|
|
+**测试通过率目标:** 连续运行 10 次,100% 通过
|
|
|
+
|
|
|
+### 后续 Story 依赖
|
|
|
+
|
|
|
+本测试完成后,后续 Story 依赖:
|
|
|
+- Story 8.5: 删除区域测试 - 可独立进行
|
|
|
+- Story 8.6: 级联选择完整流程测试 - 依赖编辑功能
|
|
|
+
|
|
|
+## Project Structure Notes
|
|
|
+
|
|
|
+### 对齐统一项目结构
|
|
|
+
|
|
|
+**目标文件位置:**
|
|
|
+```
|
|
|
+web/tests/e2e/specs/admin/region-edit.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` 导入
|
|
|
+
|
|
|
+## 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: `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/specs/admin/region-list.spec.ts`](列表查看测试)
|
|
|
+
|
|
|
+## Dev Agent Record
|
|
|
+
|
|
|
+### Agent Model Used
|
|
|
+
|
|
|
+- Model: Claude (Sonnet)
|
|
|
+- Date: 2026-01-11
|
|
|
+
|
|
|
+### Debug Log References
|
|
|
+
|
|
|
+无调试问题(Story 创建阶段)
|
|
|
+
|
|
|
+### Completion Notes List
|
|
|
+
|
|
|
+**Story 文档创建完成:**
|
|
|
+- ✅ 创建了完整的故事文档结构
|
|
|
+- ✅ 定义了所有验收标准和任务分解
|
|
|
+- ✅ 提供了详细的测试用例设计(5大类测试场景)
|
|
|
+- ✅ 包含了完整的项目上下文和参考文档
|
|
|
+
|
|
|
+**关键实现要点:**
|
|
|
+- 使用 RegionManagementPage 的 `editRegion()` 方法编辑区域信息
|
|
|
+- 使用 `toggleRegionStatus()` 方法切换区域状态
|
|
|
+- 使用 `getRegionStatus()` 验证状态更新结果
|
|
|
+- 每个测试前创建测试数据,测试后清理
|
|
|
+
|
|
|
+**预期测试数量:** 约 12-15 个测试用例
|
|
|
+
|
|
|
+### File List
|
|
|
+
|
|
|
+**Story 文档:**
|
|
|
+- `_bmad-output/implementation-artifacts/8-4-edit-region-test.md` (本文件)
|
|
|
+
|
|
|
+**待创建文件:**
|
|
|
+- `web/tests/e2e/specs/admin/region-edit.spec.ts` (目标测试文件)
|
|
|
+
|
|
|
+**参考文件 (只读):**
|
|
|
+- `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 - 内部测试工具包
|
|
|
+- Node.js 20.19.2
|
|
|
+- pnpm 10.18.3 - 包管理
|
|
|
+
|
|
|
+**测试命令:**
|
|
|
+```bash
|
|
|
+# 运行编辑区域测试
|
|
|
+cd web
|
|
|
+pnpm test:e2e:chromium region-edit
|
|
|
+
|
|
|
+# 快速失败模式(调试)
|
|
|
+timeout 60 pnpm test:e2e:chromium region-edit
|
|
|
+
|
|
|
+# 运行所有区域管理测试
|
|
|
+pnpm test:e2e:chromium region-*.spec.ts
|
|
|
+
|
|
|
+# 运行所有 E2E 测试
|
|
|
+pnpm test:e2e:chromium
|
|
|
+```
|
|
|
+
|
|
|
+**命名约定:**
|
|
|
+- 测试文件名: kebab-case + `.spec.ts` 后缀
|
|
|
+- 测试组: 使用 `test.describe.serial()` 分组
|
|
|
+- 测试名称: 中文描述,格式 "应该..."
|
|
|
+
|
|
|
+### 必须遵循的架构决策
|
|
|
+
|
|
|
+**来自 Architecture.md 的关键决策:**
|
|
|
+
|
|
|
+1. **选择器策略(混合策略优先级):**
|
|
|
+ - `data-testid` - 最高优先级
|
|
|
+ - `aria-label` + role - 无障碍标准
|
|
|
+ - Text content + role - 兜底方案
|
|
|
+
|
|
|
+2. **错误处理策略:**
|
|
|
+ - 使用 `E2ETestError` 类(来自 e2e-test-utils)
|
|
|
+ - 包含完整 ErrorContext
|
|
|
+
|
|
|
+3. **测试隔离:**
|
|
|
+ - 每个测试使用独立数据
|
|
|
+ - 测试后清理数据
|
|
|
+ - 支持并行执行(使用 `test.describe.serial` 时串行)
|
|
|
+
|
|
|
+4. **TypeScript 严格模式:**
|
|
|
+ - 所有变量必须有明确类型
|
|
|
+ - 禁止使用 `any` 类型
|
|
|
+ - 使用 `import` 配合 `vi.mocked`(Vitest)
|
|
|
+
|
|
|
+### TypeScript + Playwright 陷阱预防
|
|
|
+
|
|
|
+**来自 Architecture.md "TypeScript + Playwright 常见陷阱" 部分:**
|
|
|
+
|
|
|
+⚠️ **DOM 结构假设必须验证**
|
|
|
+- Story 8.1 已验证 DOM 结构
|
|
|
+- 使用 RegionManagementPage 的封装方法
|
|
|
+
|
|
|
+✅ **正确做法:**
|
|
|
+```typescript
|
|
|
+// 使用 Page Object 封装的方法
|
|
|
+await regionManagementPage.openEditDialog(regionName);
|
|
|
+await regionManagementPage.fillRegionForm({ name: newName });
|
|
|
+const result = await regionManagementPage.submitForm();
|
|
|
+
|
|
|
+// 验证状态时添加适当等待
|
|
|
+await regionManagementPage.page.waitForTimeout(1000);
|
|
|
+const status = await regionManagementPage.getRegionStatus(regionName);
|
|
|
+```
|
|
|
+
|
|
|
+❌ **避免:**
|
|
|
+```typescript
|
|
|
+// 避免直接操作 DOM
|
|
|
+await page.locator('.dialog .edit-button').click();
|
|
|
+
|
|
|
+// 避免假设状态立即更新
|
|
|
+const status = await getRegionStatus(regionName);
|
|
|
+expect(status).toBe('禁用'); // 可能需要等待
|
|
|
+```
|
|
|
+
|
|
|
+### 代码质量检查清单
|
|
|
+
|
|
|
+**代码质量:**
|
|
|
+- [ ] 测试用例有清晰的描述
|
|
|
+- [ ] 使用 `test.describe.serial()` 组织相关测试
|
|
|
+- [ ] 每个测试独立运行,不依赖其他测试
|
|
|
+
|
|
|
+**测试数据:**
|
|
|
+- [ ] 使用唯一标识符避免数据冲突
|
|
|
+- [ ] 测试后清理测试数据
|
|
|
+- [ ] 使用 `beforeEach`/`afterEach` 钩子
|
|
|
+
|
|
|
+**错误处理:**
|
|
|
+- [ ] 失败时有清晰的错误消息
|
|
|
+- [ ] 使用 try-catch 处理清理操作
|
|
|
+
|
|
|
+### 参考文档位置
|
|
|
+
|
|
|
+| 文档 | 路径 |
|
|
|
+|------|------|
|
|
|
+| 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` |
|
|
|
+| 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 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 测试覆盖
|
|
|
+- Epic 10: 🔄 进行中 - 订单管理 E2E 测试
|
|
|
+
|
|
|
+## Completion Status
|
|
|
+
|
|
|
+**Story ID:** 8.4
|
|
|
+**Story Key:** 8-4-edit-region-test
|
|
|
+**Epic:** Epic 8 - 区域管理 E2E 测试 (Epic B)
|
|
|
+**Status:** ready-for-dev
|
|
|
+
|
|
|
+**交付物:**
|
|
|
+- [x] Story 文档创建完成
|
|
|
+- [ ] 编辑区域测试实现(预计 12-15 个测试用例)
|
|
|
+- [ ] 测试在真实浏览器中通过
|
|
|
+- [ ] 代码审查完成
|
|
|
+
|
|
|
+**实现摘要:**
|
|
|
+- 创建了完整的 Story 8.4 文档
|
|
|
+- 定义了编辑区域测试的验收标准和任务分解
|
|
|
+- 提供了详细的测试用例设计(5 大类测试场景)
|
|
|
+- 包含了完整的项目上下文和参考文档
|
|
|
+
|
|
|
+**下一步操作:**
|
|
|
+1. 使用 `dev-story` 工作流实现测试
|
|
|
+2. 运行测试并验证通过
|
|
|
+3. 代码审查
|
|
|
+4. 进入 Story 8.5(删除区域测试)
|
|
|
+
|