9-3-note-tests.md 16 KB

Story 9.3: 备注管理功能测试

Status: done

Story

作为测试开发者, 我想要编写备注管理功能的测试, 以便验证备注的添加、修改、删除功能。

Acceptance Criteria

Given 残疾人管理 Page Object 已存在 When 编写备注管理测试 Then 包含以下测试场景:

  1. 添加备注

    • 添加简单备注
    • 添加长文本备注
    • 验证备注保存成功
  2. 修改备注

    • 编辑已有备注
    • 验证修改后内容更新
  3. 删除备注

    • 删除备注
    • 验证删除后备注消失

Tasks / Subtasks

  • [x] Task 1: 分析备注管理功能的 DOM 结构 (AC: #1, #2, #3)

    • Subtask 1.1: 在残疾人管理页面中定位备注管理区域
    • Subtask 1.2: 分析添加备注按钮和表单结构
    • Subtask 1.3: 分析备注列表展示结构
    • Subtask 1.4: 分析编辑和删除按钮的选择器
  • [x] Task 2: 创建备注测试文件 (AC: #1, #2, #3)

    • Subtask 2.1: 创建 web/tests/e2e/specs/admin/disability-person-note.spec.ts
    • Subtask 2.2: 编写添加备注测试
    • Subtask 2.3: 编写编辑备注测试
    • Subtask 2.4: 编写删除备注测试
  • [x] Task 3: 更新 Page Object (AC: #1, #2, #3)

    • Subtask 3.1: 添加备注管理相关方法到 DisabilityPersonManagementPage
    • Subtask 3.2: 实现 addNote() 方法
    • Subtask 3.3: 实现 editNote() 方法
    • Subtask 3.4: 实现 deleteNote() 方法
    • Subtask 3.5: 实现 getNoteList() 方法用于验证
  • [x] Task 4: 运行测试并验证通过 (AC: #1, #2, #3)

    • Subtask 4.1: 使用 pnpm test:e2e:chromium disability-person-note 运行测试
    • 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(本故事):备注管理功能测试
  • Story 9.4:回访记录管理测试
  • Story 9.5:完整流程测试(CRUD)
  • Story 9.6:测试隔离与并行执行验证
  • Story 9.7:稳定性验证(10次连续运行)

业务功能分析

备注管理功能概述:

残疾人管理系统中,备注功能用于记录与残疾人相关的额外信息、备注事项、沟通记录等。备注通常支持:

  • 添加多条备注(时间线模式)
  • 编辑已有备注
  • 删除备注
  • 每条备注记录创建时间和操作人

典型功能流程:

  1. 在残疾人详情页或编辑对话框中找到备注管理区域
  2. 点击"添加备注"按钮打开添加表单/对话框
  3. 填写备注内容(支持多行文本)
  4. 保存后备注显示在列表中(按时间倒序)
  5. 可以编辑或删除已添加的备注

技术规范

现有 Page Object 结构

当前 Page Object 位置: web/tests/e2e/pages/admin/disability-person.page.ts

从 Story 9.1 和 9.2 学到的模式:

  • 使用 data-testid 选择器最稳定
  • 在对话框内操作时,使用 form.getByLabel() 限制范围
  • 表单提交使用 form.handleSubmit() 并配合 console.debug 调试验证错误
  • 内联表单(如银行卡)直接在页面上操作,无需对话框

测试文件结构

web/tests/e2e/
├── specs/
│   └── admin/
│       └── disability-person-note.spec.ts  # 本测试文件(需创建)
└── pages/
    └── admin/
        └── disability-person.page.ts  # Page Object(需扩展)

测试用例设计

测试文件模板:

// web/tests/e2e/specs/admin/disability-person-note.spec.ts
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_note_${TIMESTAMP}`;

  test.beforeEach(async ({ page }) => {
    pageObject = new DisabilityPersonManagementPage(page);
    await pageObject.goto();
    await pageObject.openCreateDialog();
    await pageObject.fillBasicInfo({
      name: UNIQUE_ID,
      idCard: `110101199001011234`,
    });
  });

  test('应该成功添加简单备注', async ({ page }) => {
    const noteContent = `这是一条测试备注_${UNIQUE_ID}`;
    await pageObject.addNote(noteContent);
    const noteList = await pageObject.getNoteList();
    expect(noteList).toHaveLength(1);
    expect(noteList[0]).toContain(noteContent);
  });

  test('应该成功添加长文本备注', async ({ page }) => {
    const longNote = `这是一条很长的备注内容_${UNIQUE_ID}`.repeat(10);
    await pageObject.addNote(longNote);
    const noteList = await pageObject.getNoteList();
    expect(noteList).toHaveLength(1);
    expect(noteList[0]).toContain('这是一条很长的备注内容');
  });

  test('应该成功编辑备注', async ({ page }) => {
    const originalNote = `原始备注_${UNIQUE_ID}`;
    await pageObject.addNote(originalNote);
    const updatedNote = `更新后的备注_${UNIQUE_ID}`;
    await pageObject.editNote(0, updatedNote);
    const noteList = await pageObject.getNoteList();
    expect(noteList[0]).toContain(updatedNote);
    expect(noteList[0]).not.toContain(originalNote);
  });

  test('应该成功删除备注', async ({ page }) => {
    const noteContent = `待删除备注_${UNIQUE_ID}`;
    await pageObject.addNote(noteContent);
    let noteList = await pageObject.getNoteList();
    expect(noteList).toHaveLength(1);
    await pageObject.deleteNote(0);
    noteList = await pageObject.getNoteList();
    expect(noteList).toHaveLength(0);
  });

  test('应该支持添加多条备注', async ({ page }) => {
    await pageObject.addNote(`备注1_${UNIQUE_ID}`);
    await pageObject.addNote(`备注2_${UNIQUE_ID}`);
    await pageObject.addNote(`备注3_${UNIQUE_ID}`);
    const noteList = await pageObject.getNoteList();
    expect(noteList).toHaveLength(3);
  });
});

Page Object 方法设计

需要在 DisabilityPersonManagementPage 中添加的方法:

/**
 * 添加备注
 * @param content 备注内容
 */
async addNote(content: string): Promise<void> {
  // 1. 点击"添加备注"按钮
  // 2. 等待备注输入区域出现
  // 3. 填写备注内容
  // 4. 点击保存/确认按钮
  // 5. 等待备注出现在列表中
}

/**
 * 编辑备注
 * @param index 备注索引(第几条,从0开始)
 * @param content 更新的备注内容
 */
async editNote(index: number, content: string): Promise<void> {
  // 1. 找到指定索引的备注的编辑按钮
  // 2. 点击编辑按钮
  // 3. 等待编辑区域出现
  // 4. 更新备注内容
  // 5. 点击保存按钮
  // 6. 等待更新完成
}

/**
 * 删除备注
 * @param index 备注索引(第几条,从0开始)
 */
async deleteNote(index: number): Promise<void> {
  // 1. 找到指定索引的备注的删除按钮
  // 2. 点击删除按钮
  // 3. 等待确认对话框(如有)
  // 4. 点击确认按钮
  // 5. 等待删除完成
}

/**
 * 获取备注列表
 * @returns 备注内容数组
 */
async getNoteList(): Promise<string[]> {
  // 1. 定位备注列表容器
  // 2. 获取所有备注项的文本内容
  // 3. 返回备注信息数组
  return []; // 实现时返回真实数据
}

/**
 * 获取备注数量
 * @returns 备注数量
 */
async getNoteCount(): Promise<number> {
  return (await this.getNoteList()).length;
}

选择器策略

参考 Story 9.1 和 9.2 的经验:

  1. 添加备注按钮: 使用 page.getByRole('button', { name: /添加.*备注/ })
  2. 备注输入区域: 在表单内使用 form.getByLabel('备注内容')form.getByPlaceholderText('请输入备注')
  3. 备注列表项: 使用 data-testidrole="listitem" 选择器
  4. 编辑/删除按钮: 每个备注项内,使用索引或文本定位

示例:

// 点击编辑按钮(第1条备注的编辑按钮)
const notes = page.locator('[data-testid="note-item"]');
await notes.nth(0).getByRole('button', { name: /编辑/ }).click();

// 或使用更具体的选择器
await page.locator('[data-testid="note-item-0"] [data-testid="edit-button"]').click();

数据隔离策略

参考 Story 9.1 和 9.2 的数据隔离模式:

test.beforeEach(async ({ page }) => {
  const timestamp = Date.now();
  const uniqueId = `test_note_${timestamp}`;
  pageObject = new DisabilityPersonManagementPage(page);
  await pageObject.goto();
  await pageObject.openCreateDialog();
});

test.afterEach(async ({ page }) => {
  // 清理测试数据(根据实际业务逻辑实现)
});

项目结构说明

测试目录组织:

  • specs/: 测试用例文件,按功能模块组织,.spec.ts 后缀
  • pages/: Page Object 封装,页面元素和操作方法,复用性强

无冲突检测:

  • 新增测试文件,不影响现有代码
  • Page Object 扩展是增强,非破坏性修改

Previous Story Intelligence

Story 9.1 (照片上传功能测试) 的关键经验:

  1. 表单操作范围控制 - 使用 form.getByLabel() 限制查找范围在表单内
  2. 按钮文本获取时机 - 在点击前获取按钮文本
  3. 测试数据唯一性 - 使用 Date.now() 生成唯一 ID
  4. 超时常量定义 - 定义 TIMEOUTS 常量统一管理

Story 9.2 (银行卡管理功能测试) 的关键经验:

  1. 内联表单模式 - 银行卡管理使用内联表单(非对话框模式)
  2. 索引定位方式 - 使用 nth(index) 定位列表项
  3. 数据验证模式 - 使用 getNoteList() 获取列表后验证内容
  4. 代码审查修复 - 确保 Page Object 方法完整实现,测试代码必须使用 Page Object 方法

Git Intelligence Summary

Recent Commits:

  • a059239 - 完成 Story 10.1: 订单管理 Page Object 代码审查
  • b739b96 - docs(epic-9): 创建 Story 10.2 - 订单列表查看测试
  • 5d75cf8 - test(e2e): 完成 Story 9.2 - 银行卡管理功能测试

Code Patterns Observed:

  • 测试文件命名:disability-person-{feature}.spec.ts
  • Page Object 方法命名:动词+名词(如 addNote, editNote

TypeScript + Playwright 陷阱(关键)

基于架构文档的陷阱章节:

陷阱 1: DOM 结构假设必须验证 ⚠️

  • 备注管理功能的 DOM 结构需要在实际页面中验证
  • 使用 data-testid 选择器(最稳定)

陷阱 2: 精确文本匹配

  • 使用 :text-is() 进行精确文本匹配,而非 :has-text()

陷阱 3: 网络空闲等待

  • 备注添加/编辑后可能需要等待网络请求完成

陷阱 4: 避免使用 page.evaluate()

  • 使用 Playwright API 而非 page.evaluate() 获取元素内容

References

源文档引用:

  • [Source: _bmad-output/planning-artifacts/epics.md#Epic-9-Story-9.3] - 完整业务需求
  • [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: web/tests/e2e/pages/admin/disability-person.page.ts] - 现有 Page Object
  • [Source: allin-packages/disability-person-management-ui] - 备注管理 UI 组件(需验证 DOM 结构)

Project Structure Notes

Monorepo 结构对齐:

  • 测试位于 web/tests/e2e/ 目录
  • 使用 pnpm workspace 协议引用 @d8d/e2e-test-utils
  • 与现有 Page Object 模式保持一致

文件组织:

  • 新建测试 specs 文件:web/tests/e2e/specs/admin/disability-person-note.spec.ts
  • 扩展现有 Page Object:web/tests/e2e/pages/admin/disability-person.page.ts

遵循的项目标准:

  • 文件命名:.spec.ts 后缀(Playwright 测试)
  • 测试目录:specs/ 分离,pages/ Page Object
  • 使用 @d8d/e2e-test-utils 工具函数(如需要)
  • 遵循 docs/standards/e2e-radix-testing.md 标准

Code Review Fixes (2026-01-11)

代码审查发现并修复了以下问题:

HIGH 严重性(2个):

  • H1: ✅ Git 状态不一致 - 已确认为文档问题,实际文件已正确提交
  • H2: ✅ 数据隔离问题 - 将可变全局数组改为测试级别存储,在 beforeEach 中重置

MEDIUM 严重性(5个):

  • M1: ✅ 使用 console.log 而非 console.debug - 全部改为 console.debug(16处)
  • M2: ✅ Story 文件未在 File List 中记录 - 已记录到本文档
  • M3: ✅ 长文本备注验证不完整 - 添加了完整内容长度和值验证
  • M4: ✅ Page Object 方法命名不一致 - 移除冗余的 addRemark() 方法
  • M5: ✅ 魔法数字超时值 - 添加 TIMEOUTS 常量,替换所有魔法数字(9处)

修改的文件(代码审查):

  • web/tests/e2e/specs/admin/disability-person-note.spec.ts - 修复所有 HIGH 和 MEDIUM 问题
  • web/tests/e2e/pages/admin/disability-person.page.ts - 移除冗余方法,添加超时常量

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.3 需求(从 epics.md)
  2. ✅ 加载并分析架构文档(architecture.md)
  3. ✅ 分析前置 Story 9.1 和 9.2 的实现经验和问题
  4. ✅ 创建完整的 Story 9.3 文档,包含:

    • Story 和验收标准
    • 详细的任务分解
    • Epic 9 背景和目标
    • 业务功能分析
    • 技术规范(Page Object 方法、选择器策略)
    • 完整的测试用例模板
    • Previous Story Intelligence(从 Story 9.1 和 9.2 学到的经验)
    • Git Intelligence Summary
    • TypeScript + Playwright 陷阱警告
    • 完整的参考文档列表
  5. DOM 结构分析完成

    • 备注管理组件使用内联表单(非对话框模式)
    • 关键选择器:add-remark-button, remove-remark-{index}, remark-content-textarea-{index}, special-needs-switch-{index}
    • 最多支持 10 条备注
  6. 测试文件创建完成web/tests/e2e/specs/admin/disability-person-note.spec.ts

    • 实现了 8 个完整的测试用例
    • 使用 test.describe.serial 顺序执行(数据隔离需要)
    • 包含登录、测试执行、数据清理逻辑
  7. 所有 8 个测试通过 (100%):

    • ✅ 应该成功添加简单备注
    • ✅ 应该成功添加长文本备注
    • ✅ 应该成功编辑备注
    • ✅ 应该成功删除备注
    • ✅ 应该支持添加多条备注
    • ✅ 应该支持标记特殊需求
    • ✅ 应该限制最多添加10条备注
    • ✅ 完整流程:添加多条备注、编辑、删除
  8. Page Object 扩展完成web/tests/e2e/pages/admin/disability-person.page.ts

    • 添加了 addNote() 方法
    • 添加了 editNote() 方法
    • 添加了 deleteNote() 方法
    • 添加了 getNoteList() 方法
    • 添加了 getNoteCount() 方法
    • 添加了 getNoteSpecialNeedsStatus() 方法
    • 添加了 isAddNoteButtonDisabled() 方法

File List

创建的文件:

  • _bmad-output/implementation-artifacts/9-3-note-tests.md - 本 story 文档
  • web/tests/e2e/specs/admin/disability-person-note.spec.ts - 备注测试文件(305行,8个测试用例)

修改的文件:

  • _bmad-output/implementation-artifacts/sprint-status.yaml - 更新 Story 9.3 状态
  • web/tests/e2e/pages/admin/disability-person.page.ts - Page Object 扩展(添加备注管理方法,+105行)