Status: done
作为测试开发者, 我想要编写删除区域的 E2E 测试, 以便验证区域的删除功能和相关约束。
Given 编辑区域测试已通过 When 编写删除区域测试用例 Then 验证删除无子级区域的流程 And 验证删除有子级区域时的错误提示 And 验证删除确认对话框的正确操作 And 验证删除成功后列表中不再显示该区域 And 测试在真实浏览器中通过
web/tests/e2e/specs/admin/region-delete.spec.tsEpic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)
这是 Epic B(区域管理业务测试)的第五个 Story。前置 Story 已完成:
依赖:
区域管理支持删除操作,但有业务约束:
删除确认对话框字段(基于 AlertDialog 组件):
删除区域相关方法(来自 Story 8.1):
// 打开删除确认对话框
await regionManagementPage.openDeleteDialog('区域名称');
// 确认删除操作
await regionManagementPage.confirmDelete();
// 取消删除操作
await regionManagementPage.cancelDelete();
// 快捷方法:删除区域
const success = await regionManagementPage.deleteRegion('区域名称');
// 返回: boolean(true = 成功, false = 失败)
// 验证区域是否存在
const exists = await regionManagementPage.regionExists('区域名称');
// 返回: boolean
选择器策略(来自 Story 8.1):
getByRole('button', { name: '删除' })getByRole('button', { name: /^确认删除$/ })getByRole('button', { name: '取消' })[role="alertdialog"][data-sonner-toast][data-type="success|error"]参考 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 provinceName = `测试省_${Date.now()}`;
await regionManagementPage.createProvince({ name: provinceName });
// 删除区域
const success = await regionManagementPage.deleteRegion(provinceName);
// 验证删除成功
expect(success).toBe(true);
// 验证列表中不再显示该区域
await regionManagementPage.waitForTreeLoaded();
const exists = await regionManagementPage.regionExists(provinceName);
expect(exists).toBe(false);
});
test('应该成功删除无子级的市级区域', async ({ regionManagementPage }) => {
const provinceName = `测试省_${Date.now()}`;
const cityName = `测试市_${Date.now()}`;
// 创建省和市
await regionManagementPage.createProvince({ name: provinceName });
await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
// 删除市级区域
const success = await regionManagementPage.deleteRegion(cityName);
expect(success).toBe(true);
// 验证市级区域被删除,但省份仍存在
await regionManagementPage.waitForTreeLoaded();
expect(await regionManagementPage.regionExists(cityName)).toBe(false);
expect(await regionManagementPage.regionExists(provinceName)).toBe(true);
});
});
2. 删除有子级区域的错误处理测试:
test.describe('删除有子级区域的错误处理', () => {
test('应该阻止删除包含子级区域的父区域', async ({ regionManagementPage }) => {
const provinceName = `测试省_${Date.now()}`;
const cityName = `测试市_${Date.now()}`;
// 创建省和市
await regionManagementPage.createProvince({ name: provinceName });
await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
// 尝试删除省级区域(应该失败)
await regionManagementPage.openDeleteDialog(provinceName);
await regionManagementPage.confirmDelete();
// 等待错误提示
await regionManagementPage.page.waitForTimeout(1000);
const errorToast = regionManagementPage.page.locator('[data-sonner-toast][data-type="error"]');
const hasError = await errorToast.count() > 0;
// 验证显示错误提示
expect(hasError).toBe(true);
// 验证区域未被删除
await regionManagementPage.waitForTreeLoaded();
const provinceExists = await regionManagementPage.regionExists(provinceName);
expect(provinceExists).toBe(true);
});
test('应该显示清晰的错误消息', async ({ regionManagementPage }) => {
const provinceName = `测试省_${Date.now()}`;
const cityName = `测试市_${Date.now()}`;
// 创建省和市
await regionManagementPage.createProvince({ name: provinceName });
await regionManagementPage.createChildRegion(provinceName, '市', { name: cityName });
// 尝试删除
await regionManagementPage.deleteRegion(provinceName);
// 获取错误消息
const errorToast = regionManagementPage.page.locator('[data-sonner-toast][data-type="error"]');
const errorMessage = await errorToast.textContent();
// 验证错误消息包含关键信息
expect(errorMessage).toContain('子区域');
});
});
3. 删除确认对话框操作测试:
test.describe('删除确认对话框操作', () => {
test('取消删除应保持区域存在', async ({ regionManagementPage }) => {
const provinceName = `测试省_${Date.now()}`;
await regionManagementPage.createProvince({ name: provinceName });
// 打开删除对话框但取消
await regionManagementPage.openDeleteDialog(provinceName);
await regionManagementPage.cancelDelete();
// 验证区域仍然存在
await regionManagementPage.waitForTreeLoaded();
const exists = await regionManagementPage.regionExists(provinceName);
expect(exists).toBe(true);
});
test('确认删除应移除区域', async ({ regionManagementPage }) => {
const provinceName = `测试省_${Date.now()}`;
await regionManagementPage.createProvince({ name: provinceName });
// 确认删除
await regionManagementPage.openDeleteDialog(provinceName);
await regionManagementPage.confirmDelete();
// 验证区域被删除
await regionManagementPage.waitForTreeLoaded();
const exists = await regionManagementPage.regionExists(provinceName);
expect(exists).toBe(false);
});
});
4. 级联删除测试(先删除子级再删除父级):
test.describe('级联删除', () => {
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 });
// 先删除区级
await regionManagementPage.deleteRegion(districtName);
expect(await regionManagementPage.regionExists(districtName)).toBe(false);
// 再删除市级
await regionManagementPage.deleteRegion(cityName);
expect(await regionManagementPage.regionExists(cityName)).toBe(false);
// 最后删除省级
await regionManagementPage.deleteRegion(provinceName);
expect(await regionManagementPage.regionExists(provinceName)).toBe(false);
});
});
数据生成工具:
/**
* 生成唯一区域名称
*/
function generateUniqueRegionName(prefix: string = '测试区域'): string {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000);
return `${prefix}_${timestamp}_${random}`;
}
数据清理策略:
test.afterEach 清理每个测试创建的数据try-catch 处理清理失败的情况记录清理失败的日志
test.afterEach(async ({ regionManagementPage }) => {
if (createdProvinceName) {
try {
await regionManagementPage.deleteRegion(createdProvinceName);
createdProvinceName = '';
} catch (error) {
console.debug('清理测试数据失败:', error);
}
}
});
| 方面 | Story 8.4(编辑区域) | Story 8.5(删除区域) |
|---|---|---|
| 主要操作 | 修改现有数据 | 删除数据 |
| 前置条件 | 需要先创建测试数据 | 需要先创建测试数据 |
| 对话框类型 | 编辑对话框 | 删除确认对话框(AlertDialog) |
| 表单状态 | 预填充现有数据 | 无表单,仅确认操作 |
| 验证重点 | 数据更新、状态切换 | 数据删除、级联约束 |
| 数据清理 | 编辑后仍需删除 | 删除后无需清理 |
| 错误场景 | 名称重复、必填字段 | 有子级区域、级联约束 |
目标文件位置:
web/tests/e2e/specs/admin/region-delete.spec.ts
导入路径:
import { test, expect } from '../../utils/test-setup';
// test-setup 包含:
// - adminLoginPage fixture
// - regionManagementPage fixture
测试命令:
# 运行删除区域测试
cd web
pnpm test:e2e:chromium region-delete
# 快速失败模式(调试)
timeout 60 pnpm test:e2e:chromium region-delete
# 运行所有区域管理测试
pnpm test:e2e:chromium region-*.spec.ts
⚠️ DOM 结构假设必须验证
✅ 正确做法:
// 使用 RegionManagementPage 的封装方法
await regionManagementPage.openDeleteDialog(regionName);
await regionManagementPage.confirmDelete();
// 使用精确文本匹配验证
await expect(page.getByText('删除成功', { exact: true })).toBeVisible();
❌ 避免:
// 避免直接操作 DOM
await page.locator('.dialog .delete-button').click();
// 避免假设删除立即完成
const exists = await regionExists(regionName);
expect(exists).toBe(false); // 可能需要等待
1. 查看 DOM 结构:
# 使用 Playwright Inspector
cd web
pnpm test:e2e:chromium region-delete --debug
2. 查看错误上下文:
# 测试失败后查看
cat test-results/*/error-context.md
3. 添加调试输出:
test('调试测试', async ({ regionManagementPage }) => {
const result = await regionManagementPage.deleteRegion('测试省');
console.debug('删除结果:', result);
console.debug('Toast 消息类型:', result.toastType);
console.debug('错误消息:', result.errorMessage);
});
本 Story 的测试覆盖率:
测试通过率目标: 连续运行 10 次,100% 通过
本测试完成后,后续 Story 依赖:
目标文件位置:
web/tests/e2e/specs/admin/region-delete.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_bmad-output/implementation-artifacts/8-4-edit-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-edit.spec.tsweb/tests/e2e/utils/test-setup.ts无调试问题(Story 创建阶段)
Story 实现完成 (2026-01-11):
web/tests/e2e/specs/admin/region-delete.spec.ts 测试文件RegionManagementPage.cancelDelete() 方法中的选择器问题代码审查修复 (2026-01-11):
openDeleteDialog 方法:支持展开父节点查找子区域(市/区/街道),类似 openEditDialog 的实现waitFor() 而非 waitForTimeout()waitForTreeLoaded():在 createChildRegion 后等待树形结构刷新测试结果:
技术问题记录:
openDeleteDialog 方法已增强,现在支持展开父节点查找子区域.locator().getByRole() 模式Story 文档:
_bmad-output/implementation-artifacts/8-5-delete-region-test.md (本文件)已创建文件:
web/tests/e2e/specs/admin/region-delete.spec.ts (测试文件)已修改文件:
web/tests/e2e/pages/admin/region-management.page.ts (修复了 cancelDelete() 方法的选择器问题;增强了 openDeleteDialog() 方法以支持查找子区域)参考文件 (只读):
web/tests/e2e/specs/admin/region-add.spec.tsweb/tests/e2e/specs/admin/region-edit.spec.tsweb/tests/e2e/utils/test-setup.ts技术栈:
测试命令:
# 运行删除区域测试
cd web
pnpm test:e2e:chromium region-delete
# 快速失败模式(调试)
timeout 60 pnpm test:e2e:chromium region-delete
# 运行所有区域管理测试
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.openDeleteDialog(regionName);
await regionManagementPage.confirmDelete();
// 验证删除后等待树形结构刷新
await regionManagementPage.waitForTreeLoaded();
const exists = await regionManagementPage.regionExists(regionName);
❌ 避免:
// 避免直接操作 DOM
await page.locator('.dialog .delete-button').click();
// 避免假设状态立即更新
const exists = await regionExists(regionName);
expect(exists).toBe(false); // 可能需要等待
代码质量:
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 |
| Story 8.4 | _bmad-output/implementation-artifacts/8-4-edit-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.5 Story Key: 8-5-delete-region-test Epic: Epic 8 - 区域管理 E2E 测试 (Epic B) Status: done
交付物:
实现摘要:
web/tests/e2e/specs/admin/region-delete.spec.ts 测试文件RegionManagementPage.cancelDelete() 方法中的选择器问题测试结果:
下一步操作: