Status: ready-for-dev
作为测试开发者, 我想要编写添加区域的 E2E 测试, 以便验证省/市/区/街道的添加功能。
selectRadixOption 或 selectRadixOptionAsync 选择父级区域级联选择测试点:
web/tests/e2e/specs/admin/region-add.spec.tsEpic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)
这是 Epic B(区域管理业务测试)的第三个 Story。前置 Story 已完成:
依赖:
区域管理支持四级层级结构:
表单字段(基于 AreaForm.tsx):
添加区域相关方法(来自 Story 8.1):
// 打开新增省对话框
await regionManagementPage.openCreateProvinceDialog();
// 打开新增子区域对话框(通过父级区域)
await regionManagementPage.openAddChildDialog('广东省');
// 填写区域表单
await regionManagementPage.fillRegionForm({
name: '测试省',
code: 'TEST001',
level: 'province',
parentId: undefined,
isDisabled: false
});
// 提交表单
const result = await regionManagementPage.submitForm();
// result.success: boolean
// result.hasSuccess: boolean
// result.hasError: boolean
// result.successMessage: string | undefined
// result.errorMessage: string | undefined
// 快捷方法:创建省份
await regionManagementPage.createProvince({
name: '测试省',
code: 'TEST001'
});
// 快捷方法:创建子区域
await regionManagementPage.createChildRegion('父级区域名', {
name: '子区域',
level: 'city',
code: 'TEST002'
});
选择器策略(来自 Story 8.1 代码审查修复):
[role="dialog"]getByText('区域名称', { exact: true })[data-sonner-toast][data-type="success|error"]getByRole('button', { name: '提交' })getByRole('button', { name: '取消' })参考 web/tests/e2e/specs/admin/region-list.spec.ts(Story 8.2)的成功模式:
import { test, expect } from '@playwright/test';
import { AdminLoginPage } from '@/pages/admin/admin-login.page';
import { RegionManagementPage } from '@/pages/admin/region-management.page';
import { generateUniqueRegionName } from '@/helpers/test-data-helper';
test.describe('添加区域测试', () => {
let adminLoginPage: AdminLoginPage;
let regionManagementPage: RegionManagementPage;
test.beforeEach(async ({ page }) => {
adminLoginPage = new AdminLoginPage(page);
regionManagementPage = new RegionManagementPage(page);
// 登录
await adminLoginPage.goto();
await adminLoginPage.login('admin', 'admin123');
// 导航到区域管理页面
await regionManagementPage.goto();
});
test.afterEach(async ({ page }) => {
// 清理测试数据
// TODO: 实现数据清理逻辑
});
test('应该成功添加省级区域', async ({ page }) => {
// 测试实现
});
});
1. 添加省级区域测试:
test.describe('添加省级区域', () => {
test('应该成功添加省级区域', async ({ page }) => {
const provinceName = generateUniqueRegionName('测试省');
// 打开新增省对话框
await regionManagementPage.openCreateProvinceDialog();
// 填写表单
await regionManagementPage.fillRegionForm({
name: provinceName,
code: `PROV_${Date.now()}`,
level: 'province'
});
// 提交表单
const result = await regionManagementPage.submitForm();
expect(result.success).toBe(true);
expect(result.hasSuccess).toBe(true);
// 验证新省份出现在列表中
await regionManagementPage.waitForTreeLoaded();
const exists = await regionManagementPage.regionExists(provinceName);
expect(exists).toBe(true);
// 清理
await regionManagementPage.deleteRegion(provinceName);
});
test('添加成功后应显示成功提示消息', async ({ page }) => {
const provinceName = generateUniqueRegionName('测试省');
await regionManagementPage.openCreateProvinceDialog();
await regionManagementPage.fillRegionForm({
name: provinceName,
level: 'province'
});
await regionManagementPage.submitForm();
// 验证成功消息
await expect(regionManagementPage.page.getByTestId('toast-message'))
.toContainText('添加成功');
});
});
2. 添加市级区域测试:
test.describe('添加市级区域', () => {
test('应该成功添加市级区域', async ({ page }) => {
// 首先创建一个省份
const provinceName = generateUniqueRegionName('测试省');
await regionManagementPage.createProvince({ name: provinceName });
// 展开省份节点
await regionManagementPage.expandNode(provinceName);
// 打开新增子区域对话框
await regionManagementPage.openAddChildDialog(provinceName);
// 填写表单
const cityName = generateUniqueRegionName('测试市');
await regionManagementPage.fillRegionForm({
name: cityName,
code: `CITY_${Date.now()}`,
level: 'city'
});
// 提交表单
const result = await regionManagementPage.submitForm();
expect(result.success).toBe(true);
// 验证新城市出现在省份下
await regionManagementPage.expandNode(provinceName);
const exists = await regionManagementPage.regionExists(cityName);
expect(exists).toBe(true);
// 清理
await regionManagementPage.deleteRegion(provinceName);
});
});
3. 添加区级区域测试:
test.describe('添加区级区域', () => {
test('应该成功添加区级区域', async ({ page }) => {
// 创建省市级结构
const provinceName = generateUniqueRegionName('测试省');
const cityName = generateUniqueRegionName('测试市');
const districtName = generateUniqueRegionName('测试区');
await regionManagementPage.createProvince({ name: provinceName });
await regionManagementPage.expandNode(provinceName);
await regionManagementPage.createChildRegion(provinceName, {
name: cityName,
level: 'city'
});
await regionManagementPage.expandNode(cityName);
// 添加区级区域
await regionManagementPage.openAddChildDialog(cityName);
await regionManagementPage.fillRegionForm({
name: districtName,
level: 'district'
});
const result = await regionManagementPage.submitForm();
expect(result.success).toBe(true);
// 验证
await regionManagementPage.expandNode(cityName);
const exists = await regionManagementPage.regionExists(districtName);
expect(exists).toBe(true);
});
});
4. 添加街道级区域测试:
test.describe('添加街道级区域', () => {
test('应该成功添加街道级区域', async ({ page }) => {
// 创建省市区三级结构
const provinceName = generateUniqueRegionName('测试省');
const cityName = generateUniqueRegionName('测试市');
const districtName = generateUniqueRegionName('测试区');
const streetName = generateUniqueRegionName('测试街道');
await regionManagementPage.createProvince({ name: provinceName });
await regionManagementPage.expandNode(provinceName);
await regionManagementPage.createChildRegion(provinceName, {
name: cityName,
level: 'city'
});
await regionManagementPage.expandNode(cityName);
await regionManagementPage.createChildRegion(cityName, {
name: districtName,
level: 'district'
});
await regionManagementPage.expandNode(districtName);
// 添加街道
await regionManagementPage.openAddChildDialog(districtName);
await regionManagementPage.fillRegionForm({
name: streetName,
level: 'street'
});
const result = await regionManagementPage.submitForm();
expect(result.success).toBe(true);
// 验证
await regionManagementPage.expandNode(districtName);
const exists = await regionManagementPage.regionExists(streetName);
expect(exists).toBe(true);
});
});
5. 级联选择验证测试:
test.describe('级联选择验证', () => {
test('父级区域选择后应正确设置', async ({ page }) => {
const provinceName = generateUniqueRegionName('测试省');
await regionManagementPage.createProvince({ name: provinceName });
await regionManagementPage.expandNode(provinceName);
await regionManagementPage.openAddChildDialog(provinceName);
// 验证父级区域已正确设置
// TODO: 根据实际表单实现验证逻辑
});
test('选择父级后子区域应属于该父级', async ({ page }) => {
// 验证添加的子区域确实在父级节点下
const provinceName = generateUniqueRegionName('测试省');
const cityName = generateUniqueRegionName('测试市');
await regionManagementPage.createProvince({ name: provinceName });
await regionManagementPage.expandNode(provinceName);
await regionManagementPage.createChildRegion(provinceName, {
name: cityName,
level: 'city'
});
// 展开省份验证城市在其下
await regionManagementPage.expandNode(provinceName);
const exists = await regionManagementPage.regionExists(cityName);
expect(exists).toBe(true);
});
});
6. 表单验证测试:
test.describe('表单验证', () => {
test('未填写名称时应显示错误提示', async ({ page }) => {
await regionManagementPage.openCreateProvinceDialog();
// 不填写名称直接提交
await regionManagementPage.fillRegionForm({
name: '', // 空名称
level: 'province'
});
await regionManagementPage.submitForm();
// 验证错误提示
await expect(regionManagementPage.page.getByText('区域名称不能为空'))
.toBeVisible();
});
test('重复名称应显示错误提示', async ({ page }) => {
const provinceName = generateUniqueRegionName('测试省');
// 添加第一个省份
await regionManagementPage.createProvince({ name: provinceName });
// 尝试添加同名省份
await regionManagementPage.openCreateProvinceDialog();
await regionManagementPage.fillRegionForm({
name: provinceName, // 相同名称
level: 'province'
});
await regionManagementPage.submitForm();
// 验证错误提示
await expect(regionManagementPage.page.getByText(/已存在|重复/))
.toBeVisible();
});
});
数据生成工具(参考 Story 8.2):
/**
* 生成唯一区域名称
*/
export function generateUniqueRegionName(prefix: string = '测试区域'): string {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000);
return `${prefix}_${timestamp}_${random}`;
}
/**
* 生成唯一区域代码
*/
export function generateUniqueRegionCode(level: string): string {
const timestamp = Date.now();
return `${level.toUpperCase()}_${timestamp}`;
}
数据清理策略:
选项 3: 使用事务回滚(如可能)
test.afterEach(async ({ page }) => {
// 清理本测试创建的数据
if (createdProvinceName) {
try {
// 删除整个区域树(包含所有子区域)
await regionManagementPage.deleteRegion(createdProvinceName);
} catch (error) {
console.debug('清理测试数据失败:', error);
}
}
});
| 方面 | Story 8.2(列表查看) | Story 8.3(添加区域) |
|---|---|---|
| 主要操作 | 验证现有数据展示 | 创建新数据 |
| DOM 操作 | 展开/收起节点 | 打开对话框、填写表单 |
| 工具使用 | 主要使用 Page Object | 使用 Select 工具选择父级 |
| 数据清理 | 无需清理 | 必须清理测试数据 |
| 测试隔离 | 读取现有数据 | 创建独立数据 |
根据 packages/e2e-test-utils/src/index.ts:
// 本测试可能需要的工具
import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
// 如果表单中有父级区域下拉框选择
await selectRadixOption(page, '父级区域', '广东省');
注意: 根据区域管理的表单设计,父级区域可能是:
需要根据实际 DOM 结构确定使用哪种方式。
目标文件位置:
web/tests/e2e/specs/admin/region-add.spec.ts
导入路径:
import { AdminLoginPage } from '@/pages/admin/admin-login.page';
import { RegionManagementPage } from '@/pages/admin/region-management.page';
测试命令:
# 运行添加区域测试
cd web
pnpm test:e2e:chromium region-add
# 快速失败模式(调试)
timeout 60 pnpm test:e2e:chromium region-add
⚠️ DOM 结构假设必须验证
✅ 正确做法:
// 使用 RegionManagementPage 的封装方法
await regionManagementPage.openCreateProvinceDialog();
await regionManagementPage.fillRegionForm({ name: '测试省', level: 'province' });
await regionManagementPage.submitForm();
// 使用精确文本匹配验证
await expect(page.getByText('添加成功', { exact: true })).toBeVisible();
❌ 避免:
// 避免直接操作 DOM
await page.locator('.dialog').click();
await page.fill('input[name="name"]', '测试省');
1. 查看 DOM 结构:
# 使用 Playwright Inspector
cd web
pnpm test:e2e:chromium region-add --debug
2. 查看错误上下文:
# 测试失败后查看
cat test-results/*/error-context.md
3. 添加调试输出:
test('调试测试', async ({ page }) => {
console.debug('当前 URL:', page.url());
const result = await regionManagementPage.submitForm();
console.debug('提交结果:', result);
});
本 Story 的测试覆盖率:
测试通过率目标: 连续运行 10 次,100% 通过
本测试完成后,后续 Story 依赖:
待开发完成后填写
待开发完成后填写
待开发完成后填写
技术栈:
测试命令:
# 运行添加区域测试
cd web
pnpm test:e2e:chromium region-add
# 快速失败模式(调试)
timeout 60 pnpm test:e2e:chromium region-add
# 运行所有 E2E 测试
pnpm test:e2e:chromium
包管理:
命名约定:
.spec.ts 后缀test.describe() 分组来自 Architecture.md 的关键决策:
选择器策略(混合策略优先级):
data-testid - 最高优先级aria-label + role - 无障碍标准测试基础设施:
web/tests/e2e/specs/admin/web/tests/e2e/pages/admin/web/tests/e2e/fixtures/测试隔离:
⚠️ DOM 结构假设必须验证
✅ 正确做法:
// 使用 Page Object 封装的方法
await regionManagementPage.openCreateProvinceDialog();
await regionManagementPage.fillRegionForm(data);
await regionManagementPage.submitForm();
❌ 避免:
// 避免直接操作 DOM
await page.locator('.dialog').click();
代码质量:
test.describe() 组织相关测试测试数据:
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.2 | _bmad-output/implementation-artifacts/8-2-region-list-test.md |
| RegionManagementPage | web/tests/e2e/pages/admin/region-management.page.ts |
| 参考测试 | web/tests/e2e/specs/admin/region-list.spec.ts |
| e2e-test-utils | packages/e2e-test-utils/src/index.ts |
前置 Epic:
当前 Epic (Epic 8):
后续 Epic:
Story ID: 8.3 Story Key: 8-3-add-region-test Epic: Epic 8 - 区域管理 E2E 测试 (Epic B) Status: ready-for-dev
交付物:
下一步操作:
dev-story 工作流实现测试