|
|
@@ -0,0 +1,729 @@
|
|
|
+# Story 8.3: 编写添加区域测试
|
|
|
+
|
|
|
+Status: ready-for-dev
|
|
|
+
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
+
|
|
|
+## Story
|
|
|
+
|
|
|
+作为测试开发者,
|
|
|
+我想要编写添加区域的 E2E 测试,
|
|
|
+以便验证省/市/区/街道的添加功能。
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+
|
|
|
+1. 验证添加省级区域的流程
|
|
|
+2. 验证添加市级区域的流程(需选择父级省份)
|
|
|
+3. 验证添加区级区域的流程(需选择父级城市)
|
|
|
+4. 验证添加街道级区域的流程(需选择父级区域)
|
|
|
+5. 使用 `selectRadixOption` 或 `selectRadixOptionAsync` 选择父级区域
|
|
|
+6. 验证添加成功后列表中显示新区域
|
|
|
+7. 测试在真实浏览器中通过
|
|
|
+
|
|
|
+**级联选择测试点:**
|
|
|
+- 选择省份后,市级下拉框的选项是否正确过滤
|
|
|
+- 选择城市后,区级下拉框的选项是否正确过滤
|
|
|
+- 选择区域后,街道下拉框的选项是否正确过滤
|
|
|
+
|
|
|
+## Tasks / Subtasks
|
|
|
+
|
|
|
+- [ ] 创建测试文件基础结构 (AC: #)
|
|
|
+ - [ ] 创建 `web/tests/e2e/specs/admin/region-add.spec.ts`
|
|
|
+ - [ ] 配置 test fixtures(adminLoginPage, regionManagementPage)
|
|
|
+ - [ ] 设置测试组和 beforeEach/afterEach 钩子
|
|
|
+- [ ] 实现添加省级区域测试 (AC: 1, 6, 7)
|
|
|
+ - [ ] 测试点击"新增省"按钮打开对话框
|
|
|
+ - [ ] 测试填写省份名称
|
|
|
+ - [ ] 测试提交表单
|
|
|
+ - [ ] 验证添加成功后新省份出现在列表中
|
|
|
+- [ ] 实现添加市级区域测试 (AC: 2, 5, 6, 7)
|
|
|
+ - [ ] 测试展开省份节点
|
|
|
+ - [ ] 测试点击"新增子区域"按钮
|
|
|
+ - [ ] 测试选择父级省份(使用 selectRadixOption)
|
|
|
+ - [ ] 测试填写城市名称
|
|
|
+ - [ ] 验证添加成功后新城市出现在省份下
|
|
|
+- [ ] 实现添加区级区域测试 (AC: 3, 5, 6, 7)
|
|
|
+ - [ ] 测试展开城市节点
|
|
|
+ - [ ] 测试选择父级城市
|
|
|
+ - [ ] 测试填写区域名称
|
|
|
+ - [ ] 验证添加成功后新区域出现在城市下
|
|
|
+- [ ] 实现添加街道级区域测试 (AC: 4, 5, 6, 7)
|
|
|
+ - [ ] 测试展开区级节点
|
|
|
+ - [ ] 测试选择父级区域
|
|
|
+ - [ ] 测试填写街道名称
|
|
|
+ - [ ] 验证添加成功后新街道出现在区域下
|
|
|
+- [ ] 实现级联选择验证测试 (AC: 5, 7)
|
|
|
+ - [ ] 验证选择省份后,市级选项正确过滤
|
|
|
+ - [ ] 验证选择城市后,区级选项正确过滤
|
|
|
+ - [ ] 验证选择区域后,街道选项正确过滤
|
|
|
+- [ ] 实现表单验证测试 (AC: 7)
|
|
|
+ - [ ] 测试未填写名称时的错误提示
|
|
|
+ - [ ] 测试重复名称的处理
|
|
|
+ - [ ] 测试必填字段的验证规则
|
|
|
+- [ ] 实现测试数据隔离 (AC: #)
|
|
|
+ - [ ] 每个测试使用唯一的区域名称
|
|
|
+ - [ ] 测试后清理测试数据
|
|
|
+
|
|
|
+## Dev Notes
|
|
|
+
|
|
|
+### Epic 8 背景和上下文
|
|
|
+
|
|
|
+**Epic 8: 区域管理 E2E 测试 (Epic B - 业务测试 Epic)**
|
|
|
+
|
|
|
+这是 Epic B(区域管理业务测试)的第三个 Story。前置 Story 已完成:
|
|
|
+- Story 8.1: ✅ 已完成 - RegionManagementPage Page Object
|
|
|
+- Story 8.2: ✅ 已完成 - 区域列表查看测试
|
|
|
+
|
|
|
+**依赖:**
|
|
|
+- Epic 1: ✅ 已完成(Select 工具基础框架)
|
|
|
+- Epic 2: ✅ 已完成(Select 工具在真实 E2E 测试中验证)
|
|
|
+- Epic 3: ✅ 已完成(文件上传工具、级联选择工具)
|
|
|
+- Story 8.1: ✅ 已完成(RegionManagementPage Page Object)
|
|
|
+- Story 8.2: ✅ 已完成(区域列表查看测试)
|
|
|
+
|
|
|
+### 区域添加功能概述
|
|
|
+
|
|
|
+区域管理支持四级层级结构:
|
|
|
+1. **省级(province)** - 顶级区域,无父级
|
|
|
+2. **市级(city)** - 省级子区域
|
|
|
+3. **区级(district)** - 市级子区域
|
|
|
+4. **街道级(street)** - 区级子区域
|
|
|
+
|
|
|
+**表单字段(基于 AreaForm.tsx):**
|
|
|
+- 区域名称: 文本输入框(必填)
|
|
|
+- 区域代码: 文本输入框(可选)
|
|
|
+- 父级区域: 根据当前操作的节点自动设置或可编辑
|
|
|
+- 备注: 文本域(可选)
|
|
|
+
|
|
|
+### RegionManagementPage API 参考
|
|
|
+
|
|
|
+**添加区域相关方法(来自 Story 8.1):**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 打开新增省对话框
|
|
|
+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 })`
|
|
|
+- Toast 消息: `[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)的成功模式:
|
|
|
+
|
|
|
+```typescript
|
|
|
+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. 添加省级区域测试:**
|
|
|
+```typescript
|
|
|
+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. 添加市级区域测试:**
|
|
|
+```typescript
|
|
|
+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. 添加区级区域测试:**
|
|
|
+```typescript
|
|
|
+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. 添加街道级区域测试:**
|
|
|
+```typescript
|
|
|
+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. 级联选择验证测试:**
|
|
|
+```typescript
|
|
|
+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. 表单验证测试:**
|
|
|
+```typescript
|
|
|
+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):**
|
|
|
+```typescript
|
|
|
+/**
|
|
|
+ * 生成唯一区域名称
|
|
|
+ */
|
|
|
+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}`;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**数据清理策略:**
|
|
|
+- 选项 1: 递归删除测试创建的区域树
|
|
|
+- 选项 2: 使用 API 直接删除测试数据
|
|
|
+- 选项 3: 使用事务回滚(如可能)
|
|
|
+
|
|
|
+```typescript
|
|
|
+test.afterEach(async ({ page }) => {
|
|
|
+ // 清理本测试创建的数据
|
|
|
+ if (createdProvinceName) {
|
|
|
+ try {
|
|
|
+ // 删除整个区域树(包含所有子区域)
|
|
|
+ await regionManagementPage.deleteRegion(createdProvinceName);
|
|
|
+ } catch (error) {
|
|
|
+ console.debug('清理测试数据失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 与 Story 8.2 的关键差异
|
|
|
+
|
|
|
+| 方面 | Story 8.2(列表查看) | Story 8.3(添加区域) |
|
|
|
+|------|---------------------|---------------------|
|
|
|
+| 主要操作 | 验证现有数据展示 | 创建新数据 |
|
|
|
+| DOM 操作 | 展开/收起节点 | 打开对话框、填写表单 |
|
|
|
+| 工具使用 | 主要使用 Page Object | 使用 Select 工具选择父级 |
|
|
|
+| 数据清理 | 无需清理 | 必须清理测试数据 |
|
|
|
+| 测试隔离 | 读取现有数据 | 创建独立数据 |
|
|
|
+
|
|
|
+### 可用的 e2e-test-utils 工具
|
|
|
+
|
|
|
+根据 `packages/e2e-test-utils/src/index.ts`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 本测试可能需要的工具
|
|
|
+import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
|
|
|
+
|
|
|
+// 如果表单中有父级区域下拉框选择
|
|
|
+await selectRadixOption(page, '父级区域', '广东省');
|
|
|
+```
|
|
|
+
|
|
|
+**注意:** 根据区域管理的表单设计,父级区域可能是:
|
|
|
+1. 通过操作上下文自动设置(点击节点下的"新增子区域"按钮)
|
|
|
+2. 或者需要在表单中手动选择(如果表单有父级区域下拉框)
|
|
|
+
|
|
|
+需要根据实际 DOM 结构确定使用哪种方式。
|
|
|
+
|
|
|
+### 项目结构说明
|
|
|
+
|
|
|
+**目标文件位置:**
|
|
|
+```
|
|
|
+web/tests/e2e/specs/admin/region-add.spec.ts
|
|
|
+```
|
|
|
+
|
|
|
+**导入路径:**
|
|
|
+```typescript
|
|
|
+import { AdminLoginPage } from '@/pages/admin/admin-login.page';
|
|
|
+import { RegionManagementPage } from '@/pages/admin/region-management.page';
|
|
|
+```
|
|
|
+
|
|
|
+**测试命令:**
|
|
|
+```bash
|
|
|
+# 运行添加区域测试
|
|
|
+cd web
|
|
|
+pnpm test:e2e:chromium region-add
|
|
|
+
|
|
|
+# 快速失败模式(调试)
|
|
|
+timeout 60 pnpm test:e2e:chromium region-add
|
|
|
+```
|
|
|
+
|
|
|
+### TypeScript + Playwright 陷阱预防
|
|
|
+
|
|
|
+⚠️ **DOM 结构假设必须验证**
|
|
|
+- Story 8.1 已验证对话框和表单结构
|
|
|
+- 使用已验证的选择器策略
|
|
|
+
|
|
|
+✅ **正确做法:**
|
|
|
+```typescript
|
|
|
+// 使用 RegionManagementPage 的封装方法
|
|
|
+await regionManagementPage.openCreateProvinceDialog();
|
|
|
+await regionManagementPage.fillRegionForm({ name: '测试省', level: 'province' });
|
|
|
+await regionManagementPage.submitForm();
|
|
|
+
|
|
|
+// 使用精确文本匹配验证
|
|
|
+await expect(page.getByText('添加成功', { exact: true })).toBeVisible();
|
|
|
+```
|
|
|
+
|
|
|
+❌ **避免:**
|
|
|
+```typescript
|
|
|
+// 避免直接操作 DOM
|
|
|
+await page.locator('.dialog').click();
|
|
|
+await page.fill('input[name="name"]', '测试省');
|
|
|
+```
|
|
|
+
|
|
|
+### 测试调试技巧
|
|
|
+
|
|
|
+**1. 查看 DOM 结构:**
|
|
|
+```bash
|
|
|
+# 使用 Playwright Inspector
|
|
|
+cd web
|
|
|
+pnpm test:e2e:chromium region-add --debug
|
|
|
+```
|
|
|
+
|
|
|
+**2. 查看错误上下文:**
|
|
|
+```bash
|
|
|
+# 测试失败后查看
|
|
|
+cat test-results/*/error-context.md
|
|
|
+```
|
|
|
+
|
|
|
+**3. 添加调试输出:**
|
|
|
+```typescript
|
|
|
+test('调试测试', async ({ page }) => {
|
|
|
+ console.debug('当前 URL:', page.url());
|
|
|
+ const result = await regionManagementPage.submitForm();
|
|
|
+ console.debug('提交结果:', result);
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 测试覆盖率目标
|
|
|
+
|
|
|
+**本 Story 的测试覆盖率:**
|
|
|
+- 添加省级区域: 100%
|
|
|
+- 添加市级区域: 100%
|
|
|
+- 添加区级区域: 100%
|
|
|
+- 添加街道级区域: 100%
|
|
|
+- 级联选择验证: 100%
|
|
|
+- 表单验证: 100%
|
|
|
+
|
|
|
+**测试通过率目标:** 连续运行 10 次,100% 通过
|
|
|
+
|
|
|
+### 后续 Story 依赖
|
|
|
+
|
|
|
+本测试完成后,后续 Story 依赖:
|
|
|
+- Story 8.4: 编辑区域测试 - 依赖添加区域功能
|
|
|
+- Story 8.5: 删除区域测试 - 依赖添加区域功能
|
|
|
+
|
|
|
+## Dev Agent Record
|
|
|
+
|
|
|
+### Agent Model Used
|
|
|
+
|
|
|
+- Model: Claude (Sonnet)
|
|
|
+- Date: 2026-01-11
|
|
|
+
|
|
|
+### Debug Log References
|
|
|
+
|
|
|
+待开发完成后填写
|
|
|
+
|
|
|
+### Completion Notes List
|
|
|
+
|
|
|
+待开发完成后填写
|
|
|
+
|
|
|
+### File List
|
|
|
+
|
|
|
+待开发完成后填写
|
|
|
+
|
|
|
+## Project Context Reference
|
|
|
+
|
|
|
+### 关键项目规则摘要
|
|
|
+
|
|
|
+**技术栈:**
|
|
|
+- Playwright 1.55.0 - E2E 测试框架
|
|
|
+- TypeScript 5.9.3 - 严格模式
|
|
|
+- @d8d/e2e-test-utils - 内部测试工具包
|
|
|
+
|
|
|
+**测试命令:**
|
|
|
+```bash
|
|
|
+# 运行添加区域测试
|
|
|
+cd web
|
|
|
+pnpm test:e2e:chromium region-add
|
|
|
+
|
|
|
+# 快速失败模式(调试)
|
|
|
+timeout 60 pnpm test:e2e:chromium region-add
|
|
|
+
|
|
|
+# 运行所有 E2E 测试
|
|
|
+pnpm test:e2e:chromium
|
|
|
+```
|
|
|
+
|
|
|
+**包管理:**
|
|
|
+- 使用 pnpm(版本 10.18.3)
|
|
|
+- 内部包使用 workspace 协议
|
|
|
+
|
|
|
+**命名约定:**
|
|
|
+- 测试文件名: kebab-case + `.spec.ts` 后缀
|
|
|
+- 测试组: 使用 `test.describe()` 分组
|
|
|
+- 测试名称: 中文描述,格式 "应该..."
|
|
|
+
|
|
|
+### 必须遵循的架构决策
|
|
|
+
|
|
|
+**来自 Architecture.md 的关键决策:**
|
|
|
+
|
|
|
+1. **选择器策略(混合策略优先级):**
|
|
|
+ - `data-testid` - 最高优先级
|
|
|
+ - `aria-label` + role - 无障碍标准
|
|
|
+ - Text content + role - 兜底方案
|
|
|
+
|
|
|
+2. **测试基础设施:**
|
|
|
+ - 测试文件位置: `web/tests/e2e/specs/admin/`
|
|
|
+ - Page Object 位置: `web/tests/e2e/pages/admin/`
|
|
|
+ - Fixtures 位置: `web/tests/e2e/fixtures/`
|
|
|
+
|
|
|
+3. **测试隔离:**
|
|
|
+ - 每个测试使用独立数据
|
|
|
+ - 测试后清理数据
|
|
|
+ - 支持并行执行
|
|
|
+
|
|
|
+### TypeScript + Playwright 陷阱预防
|
|
|
+
|
|
|
+⚠️ **DOM 结构假设必须验证**
|
|
|
+- Story 8.1 已验证 DOM 结构
|
|
|
+- 使用 RegionManagementPage 的封装方法
|
|
|
+
|
|
|
+✅ **正确做法:**
|
|
|
+```typescript
|
|
|
+// 使用 Page Object 封装的方法
|
|
|
+await regionManagementPage.openCreateProvinceDialog();
|
|
|
+await regionManagementPage.fillRegionForm(data);
|
|
|
+await regionManagementPage.submitForm();
|
|
|
+```
|
|
|
+
|
|
|
+❌ **避免:**
|
|
|
+```typescript
|
|
|
+// 避免直接操作 DOM
|
|
|
+await page.locator('.dialog').click();
|
|
|
+```
|
|
|
+
|
|
|
+### 代码质量检查清单
|
|
|
+
|
|
|
+**代码质量:**
|
|
|
+- [ ] 测试用例有清晰的描述
|
|
|
+- [ ] 使用 `test.describe()` 组织相关测试
|
|
|
+- [ ] 每个测试独立运行,不依赖其他测试
|
|
|
+
|
|
|
+**测试数据:**
|
|
|
+- [ ] 使用唯一标识符避免数据冲突
|
|
|
+- [ ] 测试后清理测试数据
|
|
|
+- [ ] 使用 `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.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 和 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 测试覆盖
|
|
|
+
|
|
|
+## Completion Status
|
|
|
+
|
|
|
+**Story ID:** 8.3
|
|
|
+**Story Key:** 8-3-add-region-test
|
|
|
+**Epic:** Epic 8 - 区域管理 E2E 测试 (Epic B)
|
|
|
+**Status:** ready-for-dev
|
|
|
+
|
|
|
+**交付物:**
|
|
|
+- [x] Story 文档创建完成
|
|
|
+- [ ] 添加区域测试实现
|
|
|
+- [ ] 测试在真实浏览器中通过
|
|
|
+
|
|
|
+**下一步操作:**
|
|
|
+1. 使用 `dev-story` 工作流实现测试
|
|
|
+2. 运行测试并验证通过
|
|
|
+3. 进入 Story 8.4(编辑区域测试)
|