Status: done
作为测试开发者, 我想要编写编辑区域的 E2E 测试, 以便验证区域信息的修改功能。
Given 添加区域测试已通过 When 编写编辑区域测试用例 Then 验证编辑区域名称的流程 And 验证修改区域状态的流程(如启用/禁用) And 验证编辑后列表中正确显示更新后的信息 And 验证必填字段的验证规则 And 测试在真实浏览器中通过
web/tests/e2e/specs/admin/region-edit.spec.tscreateChildRegion 方法 - 子区域未正确关联到父节点,导致子区域编辑测试被跳过page.goto() 刷新调用waitForTimeout()Epic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)
这是 Epic B(区域管理业务测试)的第四个 Story。前置 Story 已完成:
依赖:
区域管理支持两种编辑操作:
编辑对话框字段(基于 AreaForm.tsx):
状态切换功能:
编辑区域相关方法(来自 Story 8.1):
// 打开编辑区域对话框
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('区域名称')[data-sonner-toast][data-type="success|error"].badge + text='启用'|'禁用'参考 web/tests/e2e/specs/admin/region-add.spec.ts(Story 8.3)的成功模式:
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. 编辑区域名称测试:
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. 修改区域代码测试:
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. 状态切换测试:
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. 表单验证测试:
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. 编辑子区域测试:
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);
});
});
数据生成工具:
/**
* 生成唯一区域名称
*/
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 处理清理失败的情况记录清理失败的日志
test.afterEach(async ({ regionManagementPage }) => {
if (createdProvinceName) {
try {
await regionManagementPage.deleteRegion(createdProvinceName);
createdProvinceName = '';
} catch (error) {
console.debug('清理测试数据失败:', error);
}
}
});
| 方面 | Story 8.3(添加区域) | Story 8.4(编辑区域) |
|---|---|---|
| 主要操作 | 创建新数据 | 修改现有数据 |
| 前置条件 | 无 | 需要先创建测试数据 |
| 对话框类型 | 创建对话框 | 编辑对话框 |
| 表单状态 | 空表单 | 预填充现有数据 |
| 验证重点 | 创建成功、级联选择 | 数据更新、状态切换 |
| 数据清理 | 创建后可删除 | 编辑后仍需删除 |
目标文件位置:
web/tests/e2e/specs/admin/region-edit.spec.ts
导入路径:
import { test, expect } from '../../utils/test-setup';
// test-setup 包含:
// - adminLoginPage fixture
// - regionManagementPage fixture
测试命令:
# 运行编辑区域测试
cd web
pnpm test:e2e:chromium region-edit
# 快速失败模式(调试)
timeout 60 pnpm test:e2e:chromium region-edit
# 运行所有区域管理测试
pnpm test:e2e:chromium region-*.spec.ts
⚠️ DOM 结构假设必须验证
✅ 正确做法:
// 使用 RegionManagementPage 的封装方法
await regionManagementPage.openEditDialog('区域名称');
await regionManagementPage.fillRegionForm({ name: '新名称' });
await regionManagementPage.submitForm();
// 使用精确文本匹配验证
await expect(page.getByText('更新成功', { exact: true })).toBeVisible();
❌ 避免:
// 避免直接操作 DOM
await page.locator('.dialog').click();
await page.fill('input[name="name"]', '新名称');
// 避免假设状态立即更新
const status = await page.getRegionStatus('区域名称');
expect(status).toBe('禁用'); // 可能需要等待
1. 查看 DOM 结构:
# 使用 Playwright Inspector
cd web
pnpm test:e2e:chromium region-edit --debug
2. 查看错误上下文:
# 测试失败后查看
cat test-results/*/error-context.md
3. 添加调试输出:
test('调试测试', async ({ regionManagementPage }) => {
const result = await regionManagementPage.editRegion('旧名称', { name: '新名称' });
console.debug('编辑结果:', result);
console.debug('成功消息:', result.successMessage);
console.debug('错误消息:', result.errorMessage);
});
本 Story 的测试覆盖率:
测试通过率目标: 连续运行 10 次,100% 通过
本测试完成后,后续 Story 依赖:
目标文件位置:
web/tests/e2e/specs/admin/region-edit.spec.ts
遵循模式:
web/tests/e2e/specs/admin/*.spec.tsweb/tests/e2e/pages/admin/*.page.tsweb/tests/e2e/fixtures/*.json与现有测试对齐:
test.describe.serial() 组织测试组beforeEach/afterEach 处理测试设置和清理test-setup.ts 导入源文档和规范:
_bmad-output/planning-artifacts/epics.md_bmad-output/planning-artifacts/architecture.md_bmad-output/project-context.md前置 Story 参考:
_bmad-output/implementation-artifacts/8-1-region-page-object.md_bmad-output/implementation-artifacts/8-3-add-region-test.md代码参考:
web/tests/e2e/pages/admin/region-management.page.tsweb/tests/e2e/specs/admin/region-add.spec.tsweb/tests/e2e/specs/admin/region-list.spec.ts无调试问题(Story 创建阶段)
Story 文档创建完成:
实现完成:
web/tests/e2e/specs/admin/region-edit.spec.ts 测试文件关键实现要点:
editRegion() 方法编辑区域信息toggleRegionStatus() 方法切换区域状态getRegionStatus() 验证状态更新结果已知问题:
代码审查结果 (2026-01-11):
page.goto() 调用page.goto() 调用waitForTimeout() 调用Story 文档:
_bmad-output/implementation-artifacts/8-4-edit-region-test.md (本文件)已创建文件:
web/tests/e2e/specs/admin/region-edit.spec.ts (测试文件 - 代码审查后优化)参考文件 (只读):
web/tests/e2e/specs/admin/region-add.spec.tsweb/tests/e2e/utils/test-setup.tsPage Object 引用:
web/tests/e2e/pages/admin/region-management.page.ts (由 Story 8.1 创建,本 Story 使用但未修改)技术栈:
测试命令:
# 运行编辑区域测试
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
命名约定:
.spec.ts 后缀test.describe.serial() 分组来自 Architecture.md 的关键决策:
选择器策略(混合策略优先级):
data-testid - 最高优先级aria-label + role - 无障碍标准错误处理策略:
E2ETestError 类(来自 e2e-test-utils)测试隔离:
test.describe.serial 时串行)TypeScript 严格模式:
any 类型import 配合 vi.mocked(Vitest)来自 Architecture.md "TypeScript + Playwright 常见陷阱" 部分:
⚠️ DOM 结构假设必须验证
✅ 正确做法:
// 使用 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);
❌ 避免:
// 避免直接操作 DOM
await page.locator('.dialog .edit-button').click();
// 避免假设状态立即更新
const status = await getRegionStatus(regionName);
expect(status).toBe('禁用'); // 可能需要等待
代码质量:
test.describe.serial() 组织相关测试测试数据:
beforeEach/afterEach 钩子错误处理:
| 文档 | 路径 |
|---|---|
| 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:
当前 Epic (Epic 8):
后续 Epic:
Story ID: 8.4 Story Key: 8-4-edit-region-test Epic: Epic 8 - 区域管理 E2E 测试 (Epic B) Status: done
交付物:
实现摘要:
web/tests/e2e/specs/admin/region-edit.spec.ts 测试文件代码审查结果 (2026-01-11):
page.goto() 过度刷新调用waitForTimeout() 调用已知问题和改进:
下一步操作: