|
|
@@ -0,0 +1,622 @@
|
|
|
+# Story 9.4: 回访记录管理测试
|
|
|
+
|
|
|
+Status: ready-for-dev
|
|
|
+
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
+
|
|
|
+## Story
|
|
|
+
|
|
|
+作为测试开发者,
|
|
|
+我想要编写回访记录管理的测试,
|
|
|
+以便验证回访记录的创建、查看、编辑功能。
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+
|
|
|
+**Given** 残疾人管理 Page Object 已存在
|
|
|
+**When** 编写回访记录测试
|
|
|
+**Then** 包含以下测试场景:
|
|
|
+
|
|
|
+1. **创建回访记录**
|
|
|
+ - 创建电话回访记录
|
|
|
+ - 创建上门回访记录
|
|
|
+ - 验证记录保存成功
|
|
|
+
|
|
|
+2. **查看回访历史**
|
|
|
+ - 查看残疾人的所有回访记录
|
|
|
+ - 验证记录按时间排序
|
|
|
+
|
|
|
+3. **编辑回访记录**
|
|
|
+ - 修改回访内容
|
|
|
+ - 验证修改后内容更新
|
|
|
+
|
|
|
+4. **回访记录状态管理**
|
|
|
+ - 标记回访为已完成
|
|
|
+ - 验证状态更新
|
|
|
+
|
|
|
+## Tasks / Subtasks
|
|
|
+
|
|
|
+- [ ] **Task 1: 分析回访管理功能的 DOM 结构** (AC: #1, #2, #3, #4)
|
|
|
+ - [ ] Subtask 1.1: 在残疾人管理页面中定位回访管理区域
|
|
|
+ - [ ] Subtask 1.2: 分析添加回访按钮和表单结构
|
|
|
+ - [ ] Subtask 1.3: 分析回访列表展示结构
|
|
|
+ - [ ] Subtask 1.4: 分析编辑和删除按钮的选择器
|
|
|
+
|
|
|
+- [ ] **Task 2: 创建回访记录测试文件** (AC: #1, #2, #3, #4)
|
|
|
+ - [ ] Subtask 2.1: 创建 `web/tests/e2e/specs/admin/disability-person-visit.spec.ts`
|
|
|
+ - [ ] Subtask 2.2: 编写创建回访记录测试
|
|
|
+ - [ ] Subtask 2.3: 编写查看回访历史测试
|
|
|
+ - [ ] Subtask 2.4: 编写编辑回访记录测试
|
|
|
+ - [ ] Subtask 2.5: 编写回访记录状态管理测试
|
|
|
+
|
|
|
+- [ ] **Task 3: 更新 Page Object** (AC: #1, #2, #3, #4)
|
|
|
+ - [ ] Subtask 3.1: 添加回访记录管理相关方法到 `DisabilityPersonManagementPage`
|
|
|
+ - [ ] Subtask 3.2: 完善现有的 `addVisit()` 方法
|
|
|
+ - [ ] Subtask 3.3: 实现 `editVisit()` 方法
|
|
|
+ - [ ] Subtask 3.4: 实现 `deleteVisit()` 方法
|
|
|
+ - [ ] Subtask 3.5: 实现 `getVisitList()` 方法用于验证
|
|
|
+ - [ ] Subtask 3.6: 实现 `getVisitCount()` 方法
|
|
|
+
|
|
|
+- [ ] **Task 4: 运行测试并验证通过** (AC: #1, #2, #3, #4)
|
|
|
+ - [ ] Subtask 4.1: 使用 `pnpm test:e2e:chromium disability-person-visit` 运行测试
|
|
|
+ - [ ] Subtask 4.2: 修复发现的问题
|
|
|
+ - [ ] Subtask 4.3: 验证所有测试通过
|
|
|
+
|
|
|
+## Dev Notes
|
|
|
+
|
|
|
+### Epic 9 背景与目标
|
|
|
+
|
|
|
+**Epic 9: 残疾人管理完整 E2E 测试覆盖(含并行隔离)**
|
|
|
+
|
|
|
+为残疾人管理功能编写完整的、真正验证业务功能的 E2E 测试,并确保测试可以与未来的区域管理测试并行运行。
|
|
|
+
|
|
|
+**Epic 9 的 Story 依赖关系:**
|
|
|
+- Story 9.1:照片上传功能测试 ✅ Done
|
|
|
+- Story 9.2:银行卡管理功能测试 ✅ Done
|
|
|
+- Story 9.3:备注管理功能测试 ✅ Done
|
|
|
+- **Story 9.4(本故事)**:回访记录管理测试
|
|
|
+- Story 9.5:完整流程测试(CRUD)
|
|
|
+- Story 9.6:测试隔离与并行执行验证
|
|
|
+- Story 9.7:稳定性验证(10次连续运行)
|
|
|
+
|
|
|
+### 业务功能分析
|
|
|
+
|
|
|
+**回访记录管理功能概述:**
|
|
|
+
|
|
|
+回访记录用于记录对残疾人的回访活动,包括电话回访、上门回访、视频回访等多种形式。每条回访记录包含:
|
|
|
+- 回访日期(必填,不能晚于今天)
|
|
|
+- 回访类型(必填:电话回访、上门回访、视频回访、微信回访、其他)
|
|
|
+- 回访内容(必填)
|
|
|
+- 回访结果(可选)
|
|
|
+- 下次回访日期(可选,应晚于本次回访日期)
|
|
|
+- 回访人ID(必填,默认为当前登录用户)
|
|
|
+
|
|
|
+**业务规则:**
|
|
|
+1. 最多可添加 10 条回访记录
|
|
|
+2. 回访日期不能晚于今天
|
|
|
+3. 下次回访日期应晚于本次回访日期
|
|
|
+4. 回访内容为必填项
|
|
|
+
|
|
|
+### 技术规范
|
|
|
+
|
|
|
+#### 回访管理组件 DOM 结构
|
|
|
+
|
|
|
+**组件位置:** `allin-packages/disability-person-management-ui/src/components/VisitManagement.tsx`
|
|
|
+
|
|
|
+**关键选择器(已验证):**
|
|
|
+
|
|
|
+| 元素 | data-testid | 类型 |
|
|
|
+|------|-------------|------|
|
|
|
+| 添加回访按钮 | `add-visit-button` | Button |
|
|
|
+| 删除回访按钮 | `remove-visit-{index}` | Button |
|
|
|
+| 回访日期输入 | `visit-date-input-{index}` | Input[type="date"] |
|
|
|
+| 回访类型选择器 | `visit-type-select-{index}` | Select |
|
|
|
+| 回访内容文本域 | `visit-content-textarea-{index}` | Textarea |
|
|
|
+| 回访结果输入 | `visit-result-input-{index}` | Input |
|
|
|
+| 下次回访日期输入 | `next-visit-date-input-{index}` | Input[type="date"] |
|
|
|
+| 回访人ID输入 | `visitor-id-input-{index}` | Input[type="number"] |
|
|
|
+
|
|
|
+#### 回访记录数据结构
|
|
|
+
|
|
|
+**Entity 位置:** `allin-packages/disability-module/src/entities/disabled-visit.entity.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface VisitItem {
|
|
|
+ visitDate: string; // ISO格式日期字符串
|
|
|
+ visitType: string; // 回访类型
|
|
|
+ visitContent: string; // 回访内容
|
|
|
+ visitResult?: string; // 回访结果(可选)
|
|
|
+ nextVisitDate?: string; // 下次回访日期(可选)
|
|
|
+ visitorId: number; // 回访人ID
|
|
|
+ tempId?: string; // 临时ID用于React key
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**回访类型选项:**
|
|
|
+- 电话回访
|
|
|
+- 上门回访
|
|
|
+- 视频回访
|
|
|
+- 微信回访
|
|
|
+- 其他
|
|
|
+
|
|
|
+#### 现有 Page Object 结构
|
|
|
+
|
|
|
+**当前 Page Object 位置:** `web/tests/e2e/pages/admin/disability-person.page.ts`
|
|
|
+
|
|
|
+**现有的 `addVisit()` 方法(需要完善):**
|
|
|
+
|
|
|
+当前实现(第466-505行):
|
|
|
+```typescript
|
|
|
+async addVisit(visit: {
|
|
|
+ visitDate: string;
|
|
|
+ visitType: string;
|
|
|
+ visitContent: string;
|
|
|
+ visitResult?: string;
|
|
|
+ nextVisitDate?: string;
|
|
|
+}) {
|
|
|
+ // 点击"添加回访"按钮
|
|
|
+ const addVisitButton = this.page.getByRole('button', { name: /添加回访/ });
|
|
|
+ await addVisitButton.click();
|
|
|
+ await this.page.waitForTimeout(TIMEOUTS.SHORT);
|
|
|
+
|
|
|
+ // 填写回访信息
|
|
|
+ await this.page.getByLabel(/回访日期/).fill(visit.visitDate);
|
|
|
+ await selectRadixOption(this.page, '回访类型', visit.visitType);
|
|
|
+
|
|
|
+ // 查找回访内容输入框(可能有多个,使用最后一个)
|
|
|
+ const visitContentTextarea = this.page.locator('textarea').filter({ hasText: '' }).last();
|
|
|
+ await visitContentTextarea.fill(visit.visitContent);
|
|
|
+
|
|
|
+ // 填写回访结果(可选)
|
|
|
+ if (visit.visitResult) {
|
|
|
+ const resultTextareas = this.page.locator('textarea');
|
|
|
+ const count = await resultTextareas.count();
|
|
|
+ if (count > 0) {
|
|
|
+ await resultTextareas.nth(count - 1).fill(visit.visitResult);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 填写下一次回访日期(可选)
|
|
|
+ if (visit.nextVisitDate) {
|
|
|
+ const nextDateInput = this.page.getByLabel(/下次回访日期/);
|
|
|
+ await nextDateInput.fill(visit.nextVisitDate);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(` ✓ 添加回访: ${visit.visitType} - ${visit.visitDate}`);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**问题分析:**
|
|
|
+- 使用了模糊选择器(`getByRole` 和 `getByLabel`),不够稳定
|
|
|
+- 应该使用 `data-testid` 选择器
|
|
|
+- 需要添加获取回访列表的方法用于验证
|
|
|
+
|
|
|
+### Page Object 方法设计
|
|
|
+
|
|
|
+**需要在 `DisabilityPersonManagementPage` 中添加/完善的方法:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+/**
|
|
|
+ * 添加回访记录(内联表单模式)
|
|
|
+ * @param visit 回访信息
|
|
|
+ * @returns 添加的回访记录索引
|
|
|
+ */
|
|
|
+async addVisit(visit: {
|
|
|
+ visitDate: string;
|
|
|
+ visitType: string;
|
|
|
+ visitContent: string;
|
|
|
+ visitResult?: string;
|
|
|
+ nextVisitDate?: string;
|
|
|
+ visitorId?: number;
|
|
|
+}): Promise<number> {
|
|
|
+ // 1. 点击"添加回访"按钮
|
|
|
+ // 2. 等待新的回访卡片出现
|
|
|
+ // 3. 获取当前回访数量,新添加的索引
|
|
|
+ // 4. 填写回访日期、类型、内容等
|
|
|
+ // 5. 返回新添加的回访索引
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 编辑指定索引的回访记录
|
|
|
+ * @param index 回访索引(从0开始)
|
|
|
+ * @param updatedData 更新的回访数据
|
|
|
+ */
|
|
|
+async editVisit(index: number, updatedData: {
|
|
|
+ visitDate?: string;
|
|
|
+ visitType?: string;
|
|
|
+ visitContent?: string;
|
|
|
+ visitResult?: string;
|
|
|
+ nextVisitDate?: string;
|
|
|
+}): Promise<void> {
|
|
|
+ // 1. 直接在对应的输入框中修改值
|
|
|
+ // 2. 验证修改完成
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 删除指定索引的回访记录
|
|
|
+ * @param index 回访索引(从0开始)
|
|
|
+ */
|
|
|
+async deleteVisit(index: number): Promise<void> {
|
|
|
+ // 1. 点击删除按钮
|
|
|
+ // 2. 等待删除完成
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取回访记录列表
|
|
|
+ * @returns 回访信息数组
|
|
|
+ */
|
|
|
+async getVisitList(): Promise<Array<{
|
|
|
+ visitDate: string;
|
|
|
+ visitType: string;
|
|
|
+ visitContent: string;
|
|
|
+ visitResult?: string;
|
|
|
+ nextVisitDate?: string;
|
|
|
+}>> {
|
|
|
+ // 1. 遍历所有回访卡片
|
|
|
+ // 2. 读取每个字段的值
|
|
|
+ // 3. 返回回访信息数组
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取当前回访记录数量
|
|
|
+ * @returns 回访数量
|
|
|
+ */
|
|
|
+async getVisitCount(): Promise<number> {
|
|
|
+ return await this.page.locator('[data-testid^="remove-visit-"]').count();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 检查添加回访按钮是否被禁用
|
|
|
+ * @returns 是否禁用
|
|
|
+ */
|
|
|
+async isAddVisitButtonDisabled(): Promise<boolean> {
|
|
|
+ const addButton = this.page.locator('[data-testid="add-visit-button"]');
|
|
|
+ return await addButton.isDisabled();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 滚动到回访记录区域
|
|
|
+ */
|
|
|
+async scrollToVisitSection(): Promise<void> {
|
|
|
+ const visitLabel = this.page.getByText('回访记录管理');
|
|
|
+ await visitLabel.scrollIntoViewIfNeeded();
|
|
|
+ await this.page.waitForTimeout(TIMEOUTS.SHORT);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 测试用例设计
|
|
|
+
|
|
|
+**测试文件:** `web/tests/e2e/specs/admin/disability-person-visit.spec.ts`
|
|
|
+
|
|
|
+**测试用例模板:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { test, expect } from '@playwright/test';
|
|
|
+import { DisabilityPersonManagementPage } from '../../pages/admin/disability-person.page';
|
|
|
+
|
|
|
+test.describe('残疾人管理 - 回访记录管理功能', () => {
|
|
|
+ let pageObject: DisabilityPersonManagementPage;
|
|
|
+ const TIMESTAMP = Date.now();
|
|
|
+ const UNIQUE_ID = `test_visit_${TIMESTAMP}`;
|
|
|
+
|
|
|
+ test.beforeEach(async ({ page }) => {
|
|
|
+ pageObject = new DisabilityPersonManagementPage(page);
|
|
|
+ await pageObject.goto();
|
|
|
+ await pageObject.openCreateDialog();
|
|
|
+ await pageObject.fillBasicInfo({
|
|
|
+ name: UNIQUE_ID,
|
|
|
+ gender: '男',
|
|
|
+ idCard: `110101199001011234`,
|
|
|
+ disabilityId: `1234567890${TIMESTAMP}`,
|
|
|
+ disabilityType: '肢体残疾',
|
|
|
+ disabilityLevel: '一级',
|
|
|
+ phone: '13800138000',
|
|
|
+ idAddress: '北京市东城区测试街道1号',
|
|
|
+ province: '北京市',
|
|
|
+ city: '北京市',
|
|
|
+ });
|
|
|
+ await pageObject.scrollToVisitSection();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该成功创建电话回访记录', async ({ page }) => {
|
|
|
+ const today = new Date().toISOString().split('T')[0];
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '电话回访',
|
|
|
+ visitContent: `电话回访测试内容_${UNIQUE_ID}`,
|
|
|
+ visitResult: '良好',
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ const visitCount = await pageObject.getVisitCount();
|
|
|
+ expect(visitCount).toBe(1);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该成功创建上门回访记录', async ({ page }) => {
|
|
|
+ const today = new Date().toISOString().split('T')[0];
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '上门回访',
|
|
|
+ visitContent: `上门回访测试内容_${UNIQUE_ID}`,
|
|
|
+ visitResult: '需要跟进',
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ const visitCount = await pageObject.getVisitCount();
|
|
|
+ expect(visitCount).toBe(1);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该成功编辑回访记录内容', async ({ page }) => {
|
|
|
+ const today = new Date().toISOString().split('T')[0];
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '电话回访',
|
|
|
+ visitContent: `原始内容_${UNIQUE_ID}`,
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ const updatedContent = `更新后的回访内容_${UNIQUE_ID}`;
|
|
|
+ await pageObject.editVisit(0, { visitContent: updatedContent });
|
|
|
+ const visitList = await pageObject.getVisitList();
|
|
|
+ expect(visitList[0].visitContent).toContain('更新后的回访内容');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该成功删除回访记录', async ({ page }) => {
|
|
|
+ const today = new Date().toISOString().split('T')[0];
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '电话回访',
|
|
|
+ visitContent: `待删除内容_${UNIQUE_ID}`,
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ let visitCount = await pageObject.getVisitCount();
|
|
|
+ expect(visitCount).toBe(1);
|
|
|
+ await pageObject.deleteVisit(0);
|
|
|
+ visitCount = await pageObject.getVisitCount();
|
|
|
+ expect(visitCount).toBe(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该支持添加多条回访记录', async ({ page }) => {
|
|
|
+ const today = new Date().toISOString().split('T')[0];
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '电话回访',
|
|
|
+ visitContent: `回访1_${UNIQUE_ID}`,
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '上门回访',
|
|
|
+ visitContent: `回访2_${UNIQUE_ID}`,
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '视频回访',
|
|
|
+ visitContent: `回访3_${UNIQUE_ID}`,
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ const visitCount = await pageObject.getVisitCount();
|
|
|
+ expect(visitCount).toBe(3);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该限制最多添加10条回访记录', async ({ page }) => {
|
|
|
+ const today = new Date().toISOString().split('T')[0];
|
|
|
+ // 添加10条回访记录
|
|
|
+ for (let i = 0; i < 10; i++) {
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '电话回访',
|
|
|
+ visitContent: `回访${i + 1}_${UNIQUE_ID}`,
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 验证按钮被禁用
|
|
|
+ const isDisabled = await pageObject.isAddVisitButtonDisabled();
|
|
|
+ expect(isDisabled).toBe(true);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该正确显示回访记录历史', async ({ page }) => {
|
|
|
+ const today = new Date().toISOString().split('T')[0];
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '电话回访',
|
|
|
+ visitContent: `第一次回访_${UNIQUE_ID}`,
|
|
|
+ visitResult: '良好',
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ await pageObject.addVisit({
|
|
|
+ visitDate: today,
|
|
|
+ visitType: '上门回访',
|
|
|
+ visitContent: `第二次回访_${UNIQUE_ID}`,
|
|
|
+ visitResult: '需要跟进',
|
|
|
+ visitorId: 1,
|
|
|
+ });
|
|
|
+ const visitList = await pageObject.getVisitList();
|
|
|
+ expect(visitList).toHaveLength(2);
|
|
|
+ expect(visitList[0].visitContent).toContain('第一次回访');
|
|
|
+ expect(visitList[1].visitContent).toContain('第二次回访');
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 选择器策略
|
|
|
+
|
|
|
+**参考 Story 9.1、9.2、9.3 的经验,使用 data-testid 选择器:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 添加回访按钮
|
|
|
+const addButton = this.page.locator('[data-testid="add-visit-button"]');
|
|
|
+
|
|
|
+// 删除回访按钮(索引)
|
|
|
+const removeButton = this.page.locator(`[data-testid="remove-visit-${index}"]`);
|
|
|
+
|
|
|
+// 回访日期输入
|
|
|
+const dateInput = this.page.locator(`[data-testid="visit-date-input-${index}"]`);
|
|
|
+
|
|
|
+// 回访类型选择器 - 使用 Radix Select 工具
|
|
|
+const typeSelectTrigger = this.page.locator(`[data-testid="visit-type-select-${index}"]`);
|
|
|
+
|
|
|
+// 回访内容文本域
|
|
|
+const contentTextarea = this.page.locator(`[data-testid="visit-content-textarea-${index}"]`);
|
|
|
+
|
|
|
+// 回访结果输入
|
|
|
+const resultInput = this.page.locator(`[data-testid="visit-result-input-${index}"]`);
|
|
|
+
|
|
|
+// 下次回访日期输入
|
|
|
+const nextDateInput = this.page.locator(`[data-testid="next-visit-date-input-${index}"]`);
|
|
|
+
|
|
|
+// 回访人ID输入
|
|
|
+const visitorIdInput = this.page.locator(`[data-testid="visitor-id-input-${index}"]`);
|
|
|
+```
|
|
|
+
|
|
|
+### 数据隔离策略
|
|
|
+
|
|
|
+**参考 Story 9.1、9.2、9.3 的数据隔离模式:**
|
|
|
+
|
|
|
+```typescript
|
|
|
+test.beforeEach(async ({ page }) => {
|
|
|
+ const timestamp = Date.now();
|
|
|
+ const uniqueId = `test_visit_${timestamp}`;
|
|
|
+ pageObject = new DisabilityPersonManagementPage(page);
|
|
|
+ await pageObject.goto();
|
|
|
+ await pageObject.openCreateDialog();
|
|
|
+ // 使用唯一ID确保数据隔离
|
|
|
+});
|
|
|
+
|
|
|
+test.afterEach(async ({ page }) => {
|
|
|
+ // 清理测试数据(如需要)
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 项目结构说明
|
|
|
+
|
|
|
+**测试目录组织:**
|
|
|
+- **specs/**: 测试用例文件,按功能模块组织,`.spec.ts` 后缀
|
|
|
+- **pages/**: Page Object 封装,页面元素和操作方法,复用性强
|
|
|
+
|
|
|
+**无冲突检测:**
|
|
|
+- 新增测试文件,不影响现有代码
|
|
|
+- Page Object 扩展是增强,非破坏性修改
|
|
|
+
|
|
|
+**文件组织:**
|
|
|
+- 新建测试 specs 文件:`web/tests/e2e/specs/admin/disability-person-visit.spec.ts`
|
|
|
+- 扩展现有 Page Object:`web/tests/e2e/pages/admin/disability-person.page.ts`
|
|
|
+
|
|
|
+### Previous Story Intelligence
|
|
|
+
|
|
|
+**Story 9.1 (照片上传功能测试) 的关键经验:**
|
|
|
+
|
|
|
+1. **表单操作范围控制** - 使用 `form.getByLabel()` 限制查找范围在表单内
|
|
|
+2. **按钮文本获取时机** - 在点击前获取按钮文本
|
|
|
+3. **测试数据唯一性** - 使用 `Date.now()` 生成唯一 ID
|
|
|
+4. **超时常量定义** - 定义 `TIMEOUTS` 常量统一管理
|
|
|
+
|
|
|
+**Story 9.2 (银行卡管理功能测试) 的关键经验:**
|
|
|
+
|
|
|
+1. **内联表单模式** - 银行卡管理使用内联表单(非对话框模式),回访记录也是内联模式
|
|
|
+2. **索引定位方式** - 使用 `nth(index)` 定位列表项
|
|
|
+3. **数据验证模式** - 使用 `getVisitList()` 获取列表后验证内容
|
|
|
+
|
|
|
+**Story 9.3 (备注管理功能测试) 的关键经验:**
|
|
|
+
|
|
|
+1. **完整实现 Page Object 方法** - 代码审查发现必须完整实现所有方法
|
|
|
+2. **测试必须使用 Page Object** - 不能直接操作 page,必须通过 Page Object 方法
|
|
|
+3. **console.debug 调试** - 使用 `console.debug` 而非 `console.log`(Vitest 中只有 debug 会显示)
|
|
|
+4. **超时常量替换魔法数字** - 使用 `TIMEOUTS` 常量替换所有魔法数字
|
|
|
+
|
|
|
+### Git Intelligence Summary
|
|
|
+
|
|
|
+**Recent Commits:**
|
|
|
+- `e589d94b` - test(e2e): 完成 Story 9.3 - 备注管理功能测试
|
|
|
+- `2be3dd84` - test(e2e): 完成 Story 10.2 代码审查 - 订单列表查看测试
|
|
|
+- `8c36a373` - test(e2e): 完成 Story 10.2 - 订单列表查看测试
|
|
|
+
|
|
|
+**Code Patterns Observed:**
|
|
|
+- 测试文件命名:`disability-person-{feature}.spec.ts`
|
|
|
+- Page Object 方法命名:动词+名词(如 `addVisit`, `editVisit`)
|
|
|
+- 使用 `data-testid` 选择器确保稳定性
|
|
|
+
|
|
|
+### TypeScript + Playwright 陷阱(关键)
|
|
|
+
|
|
|
+基于架构文档的陷阱章节和前置 Story 的经验:
|
|
|
+
|
|
|
+**陷阱 1: DOM 结构假设必须验证** ⚠️
|
|
|
+- 回访记录管理功能的 DOM 结构已在 `VisitManagement.tsx` 中验证
|
|
|
+- 使用 `data-testid` 选择器(最稳定)
|
|
|
+
|
|
|
+**陷阱 2: 精确文本匹配**
|
|
|
+- 使用 `:text-is()` 进行精确文本匹配,而非 `:has-text()`
|
|
|
+
|
|
|
+**陷阱 3: 网络空闲等待**
|
|
|
+- 回访添加/编辑后可能需要等待网络请求完成
|
|
|
+
|
|
|
+**陷阱 4: 避免使用 page.evaluate()**
|
|
|
+- 使用 Playwright API 而非 `page.evaluate()` 获取元素内容
|
|
|
+
|
|
|
+**陷阱 5: Select 组件操作**
|
|
|
+- 回访类型使用 Radix UI Select,必须使用 `selectRadixOption` 工具
|
|
|
+- Select 触发器使用 `data-testid="visit-type-select-{index}"`
|
|
|
+
|
|
|
+### References
|
|
|
+
|
|
|
+**源文档引用:**
|
|
|
+- [Source: _bmad-output/planning-artifacts/epics.md#Epic-9-Story-9.4] - 完整业务需求
|
|
|
+- [Source: _bmad-output/planning-artifacts/architecture.md#Testing-Configuration] - 三层测试策略
|
|
|
+- [Source: docs/standards/e2e-radix-testing.md] - E2E 测试标准
|
|
|
+
|
|
|
+**前置 Story 参考:**
|
|
|
+- [Source: _bmad-output/implementation-artifacts/9-1-photo-upload-tests.md] - 照片上传测试实现经验
|
|
|
+- [Source: _bmad-output/implementation-artifacts/9-2-bankcard-tests.md] - 银行卡测试实现经验
|
|
|
+- [Source: _bmad-output/implementation-artifacts/9-3-note-tests.md] - 备注测试实现经验
|
|
|
+
|
|
|
+**相关组件源码:**
|
|
|
+- [Source: web/tests/e2e/pages/admin/disability-person.page.ts] - 现有 Page Object
|
|
|
+- [Source: allin-packages/disability-person-management-ui/src/components/VisitManagement.tsx] - 回访管理 UI 组件
|
|
|
+- [Source: allin-packages/disability-module/src/entities/disabled-visit.entity.ts] - 回访记录 Entity
|
|
|
+
|
|
|
+### Project Structure Notes
|
|
|
+
|
|
|
+**Monorepo 结构对齐:**
|
|
|
+- 测试位于 `web/tests/e2e/` 目录
|
|
|
+- 使用 pnpm workspace 协议引用 `@d8d/e2e-test-utils`
|
|
|
+- 与现有 Page Object 模式保持一致
|
|
|
+
|
|
|
+**遵循的项目标准:**
|
|
|
+- 文件命名:`.spec.ts` 后缀(Playwright 测试)
|
|
|
+- 测试目录:`specs/` 分离,`pages/` Page Object
|
|
|
+- 使用 `@d8d/e2e-test-utils` 工具函数(`selectRadixOption`)
|
|
|
+- 遵循 `docs/standards/e2e-radix-testing.md` 标准
|
|
|
+
|
|
|
+## Dev Agent Record
|
|
|
+
|
|
|
+### Agent Model Used
|
|
|
+
|
|
|
+Claude Opus 4 (claude-opus-4-5-20251101)
|
|
|
+
|
|
|
+### Debug Log References
|
|
|
+
|
|
|
+### Completion Notes List
|
|
|
+
|
|
|
+1. ✅ 加载并分析 Epic 9 Story 9.4 需求(从 epics.md)
|
|
|
+2. ✅ 加载并分析架构文档(architecture.md)
|
|
|
+3. ✅ 分析前置 Story 9.1、9.2、9.3 的实现经验和问题
|
|
|
+4. ✅ 分析回访管理组件 DOM 结构(VisitManagement.tsx)
|
|
|
+5. ✅ 分析回访记录数据结构(disabled-visit.entity.ts)
|
|
|
+6. ✅ 创建完整的 Story 9.4 文档,包含:
|
|
|
+ - Story 和验收标准
|
|
|
+ - 详细的任务分解
|
|
|
+ - Epic 9 背景和目标
|
|
|
+ - 业务功能分析(业务规则、数据结构)
|
|
|
+ - 技术规范(DOM 结构、Page Object 方法)
|
|
|
+ - 完整的测试用例模板
|
|
|
+ - Previous Story Intelligence(从 Story 9.1、9.2、9.3 学到的经验)
|
|
|
+ - Git Intelligence Summary
|
|
|
+ - TypeScript + Playwright 陷阱警告
|
|
|
+ - 完整的参考文档列表
|
|
|
+
|
|
|
+### File List
|
|
|
+
|
|
|
+**待创建的文件:**
|
|
|
+- `_bmad-output/implementation-artifacts/9-4-visit-tests.md` - 本 story 文档
|
|
|
+- `web/tests/e2e/specs/admin/disability-person-visit.spec.ts` - 回访测试文件(待实现)
|
|
|
+
|
|
|
+**待修改的文件:**
|
|
|
+- `_bmad-output/implementation-artifacts/sprint-status.yaml` - 更新 Story 9.4 状态
|
|
|
+- `web/tests/e2e/pages/admin/disability-person.page.ts` - Page Object 扩展(待实现)
|