Files
hms/docs/superpowers/specs/2026-05-03-web-page-component-testing-design.md
iven 1602b7bbad
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs(wiki): Wiki 全面刷新 + Q2 路线图 + 测试补强设计规格
- Wiki 7 文件关键数字刷新:迁移 96→103、实体 45→46、前端 163→225、测试 5→36
- 修复 architecture.md PostgreSQL 版本不一致(18→16)
- 修复 erp-ai.md 实体数 3→6、erp-health.md 实体数 45→46
- 更新 index.md 文档索引:specs 41、plans 38、discussions 18
- 新增事件注册表/方法论/分析报告引用
- 新增页面/组件测试设计规格(模式化工厂方案)
- 新增 Q2 路线图规格(技术债 + 新功能并行 8 周)
2026-05-03 22:33:08 +08:00

447 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Web 前端页面/组件全面测试设计规格
> 日期: 2026-05-03 | 类型: 设计规格 | 状态: 定稿
>
> **目标:** 为 HMS Web 前端页面/组件建立全面测试覆盖,从当前 ~3% 页面覆盖率提升到 85%(约 85/99 个页面有测试文件)。
## 1. 背景与动机
### 为什么做
2026-05-03 深度分析报告中4 个专家组独立将「前端测试空白」标记为最大质量风险。虽然此后的工作已完成 Store 测试6 个)和 API 契约测试25 个),但 **99 个页面/组件中仅有 1 个StatusTag有测试**
页面级测试的缺失意味着:
- 任何重构都无法验证不引入回归
- 错误处理统一化18 文件)已执行但无测试保障
- 大组件拆分5 个 500+ 行文件)因无安全网而风险过高
### 范围
- **覆盖:** Web 前端 99 个页面/组件 TSX 文件
- **不含:** Store 测试已完成、API 契约测试已完成、E2E 测试(独立维护)
- **工具:** Vitest + @testing-library/react + msw
## 2. 页面模式分类
分析 99 个页面组件后识别出 4 种核心交互模式:
### 模式 1列表页ListPage— ~50 个页面
**特征:** Ant Design Table + FilterBar + 分页 + 新建/编辑弹窗
**示例:** PatientList, AppointmentList, AlertList, DoctorList, Users, Roles, Organizations, Messages, ArticleManageList, PointsProductList, PointsOrderList, PointsRuleList, AlertRuleList, FollowUpTaskList, FollowUpRecordList, FollowUpTemplateList, ConsultationList, DeviceManage, DialysisManageList, OfflineEventList, DictionaryManager, MenuConfig, NumberingRules, PluginAdmin, PluginMarket, AuditLogViewer, PendingTasks, CompletedTasks, ProcessDefinitions, InstanceMonitor
**测试要点:**
1. 初始渲染调用列表 API
2. 表格渲染正确的列名
3. 分页翻页传递正确参数
4. 筛选器传递正确参数
5. 新建按钮可点击
6. 编辑按钮可点击
### 模式 2详情页DetailPage— ~15 个页面
**特征:** 多 Tab 切换 + 子组件按 Tab 加载
**示例:** PatientDetail, ConsultationDetail, PatientDetail 内的 Tab 组件VitalSignsTab, LabReportsTab, FollowUpTab, FamilyMembersTab, HealthRecordsTab, DeviceReadingsTab, PointsAccountTab, DailyMonitoringTab, AiSuggestionTab
**测试要点:**
1. URL 参数正确传递
2. Tab 切换加载对应数据
3. 返回导航正常
### 模式 3表单页FormPage— ~10 个页面
**特征:** 独立表单页面 + 提交/保存
**示例:** ArticleEditor, Login, ArticleCategoryManage, ArticleTagManage, PatientTagManage, ChangePassword, ThemeSettings, LanguageManager, SystemSettings
**测试要点:**
1. 表单字段正确渲染
2. 必填校验触发
3. 提交调用正确 API
4. 成功后正确跳转或提示
### 模式 4仪表盘页DashboardPage— ~10 个页面
**特征:** 统计卡片 + 图表 + 数据聚合
**示例:** Home, StatisticsDashboard, AlertDashboard, AiUsageDashboard, AiAnalysisList, AiPromptList, AiUsageDashboard, DashboardWidgets
**测试要点:**
1. 统计 API 正确调用
2. 数据渲染到卡片
3. 时间范围切换传参正确
### 不归类 — ~14 个复杂页面
PluginCRUDPage, PluginDashboardPage, PluginGraphPage, PluginKanbanPage, PluginTabsPage, PluginTreePage, ProcessDesigner, ProcessViewer, Settings容器页, Workflow容器页, CalendarView, VitalSignsChart, ImagePreview, AlertDetailPanel
这些页面需要独立手写测试。
## 3. 技术架构
### 3.1 工具链
```
vitest 4 + @testing-library/react 16 + msw 2
```
**选择 msw 的理由:**
- 拦截层在网络层,与 axios/fetch 解耦
- API mock handlers 可与已有 API 契约测试共享
- 支持真实 HTTP 状态码和错误场景模拟
- 不需要在每个测试文件中手动 jest.fn()
### 3.2 文件结构
```
apps/web/src/test/
├── setup.ts # 已有jsdom setup + @testing-library/jest-dom
├── factories/
│ ├── listPageTests.ts # createListPageTests() — 列表页测试工厂
│ ├── detailPageTests.ts # createDetailPageTests() — 详情页测试工厂
│ ├── formPageTests.ts # createFormPageTests() — 表单页测试工厂
│ └── dashboardPageTests.ts # createDashboardPageTests() — 仪表盘测试工厂
├── helpers/
│ ├── renderWithProviders.tsx # Router + Zustand Store + AntD ConfigProvider 包裹
│ ├── mswServer.ts # msw server setup/teardown
│ ├── apiHandlers.ts # 通用 API mock handlers与契约测试共享
│ └── testFixtures.ts # 测试数据工厂(患者、医生、预约等)
```
### 3.3 工厂函数接口
```typescript
// 列表页测试工厂
interface ListPageTestConfig {
component: React.ComponentType;
apiUrl: string;
columns: string[];
filters?: Array<{ name: string; type: 'select' | 'input' | 'date' }>;
hasCreate?: boolean;
hasEdit?: boolean;
hasDelete?: boolean;
mockData: Record<string, unknown>[];
}
function createListPageTests(config: ListPageTestConfig): void;
// 详情页测试工厂
interface DetailPageTestConfig {
component: React.ComponentType;
detailApiUrl: string;
tabs: Array<{ label: string; apiUrl: string }>;
mockDetail: Record<string, unknown>;
}
function createDetailPageTests(config: DetailPageTestConfig): void;
// 表单页测试工厂
interface FormPageTestConfig {
component: React.ComponentType;
submitApiUrl: string;
submitMethod: 'POST' | 'PUT';
requiredFields: string[];
mockInitialValues?: Record<string, unknown>;
}
function createFormPageTests(config: FormPageTestConfig): void;
// 仪表盘测试工厂
interface DashboardTestConfig {
component: React.ComponentType;
statsApis: Array<{ url: string; responseKey: string }>;
hasDateRangePicker?: boolean;
}
function createDashboardPageTests(config: DashboardTestConfig): void;
```
### 3.4 renderWithProviders
```typescript
// 统一包裹组件,模拟真实运行环境
function renderWithProviders(
ui: React.ReactElement,
options?: {
initialRoute?: string;
preloadedState?: Record<string, unknown>;
userRole?: 'admin' | 'doctor' | 'nurse';
}
): {
...RenderResult;
store: ReturnType<typeof useStore>;
};
```
包裹内容:
- `<MemoryRouter>` — 模拟路由
- Zustand store 重置 + 预填充 — Zustand v5 不使用 Provider通过 `zustand/testing` 或直接 store.setState 注入测试状态
- Ant Design `<ConfigProvider>` — 中文 locale + 主题(注意 antd v6 的 Modal 默认渲染到 document.body需设置 `getContainer` 或使用 `container.baseElement`
## 4. 执行策略
### 4.1 分批计划
#### 第一批(~20 个测试文件):健康管理核心流程
优先覆盖患者和预约管理,这是所有业务的基础。
**工厂开发 + 核心页面:**
| 页面 | 模式 | 说明 |
|------|------|------|
| PatientList | ListPage | 患者列表 — 验证工厂 |
| PatientDetail | DetailPage | 患者详情 — 验证工厂 |
| AppointmentList | ListPage | 预约列表 |
| FollowUpTaskList | ListPage | 随访任务 |
| FollowUpRecordList | ListPage | 随访记录 |
| AlertList | ListPage | 告警列表 |
| AlertDashboard | DashboardPage | 告警仪表盘 |
| ConsultationList | ListPage | 咨询列表 |
| ConsultationDetail | DetailPage | 咨询详情 |
| DoctorList | ListPage | 医生列表 |
| DoctorSchedule | FormPage | 医生排班 |
| VitalSignsTab | DetailPage(sub) | 体征 Tab |
| LabReportsTab | DetailPage(sub) | 化验 Tab |
| StatusTag | 已有 | 补充更多场景 |
| PatientSelect | FormPage(sub) | 患者选择器 |
| DoctorSelect | FormPage(sub) | 医生选择器 |
| FilterBar | 组件测试 | 筛选栏 |
| ExportButton | 组件测试 | 导出按钮 |
| AiSuggestionTab | DetailPage(sub) | AI 建议 Tab |
| HealthRecordsTab | DetailPage(sub) | 健康记录 Tab |
#### 第二批(~30 个测试文件):其余健康管理 + 基础模块
| 域 | 页面数 | 模式 |
|----|--------|------|
| 积分系统 | 3 (PointsProductList, PointsOrderList, PointsRuleList) | ListPage |
| 内容管理 | 4 (ArticleManageList, ArticleEditor, ArticleCategoryManage, ArticleTagManage) | ListPage + FormPage |
| 设备 + 透析 + 线下活动 | 3 | ListPage |
| 基础Users, Roles, Organizations, Messages | 4 | ListPage |
| Login | 1 | FormPage |
| 健康管理剩余 | ~14 | 混合 |
#### 第三批(~20 个测试文件):设置 + 工作流 + 插件 + 复杂页面
| 域 | 页面数 | 说明 |
|----|--------|------|
| 设置页面 | 8 | ListPage + FormPage |
| 工作流 | 6 | 混合ProcessDesigner 手写) |
| 插件 | 7 | 混合(多个需手写) |
| 仪表盘 | 3 | DashboardPage |
| 复杂页面手写 | ~14 | 独立测试 |
### 4.2 覆盖率目标
| 阶段 | 新增测试文件 | 累计测试文件 | 页面覆盖率 |
|------|------------|------------|-----------|
| 当前 | 0 | 36 | ~3% (1/99) |
| 第一批完成 | +20 | 56 | ~21% |
| 第二批完成 | +30 | 86 | ~51% |
| 第三批完成 | +20 | 106 | ~85%(含手写 14 |
> **页面覆盖率 = 有测试文件的页面数 / 总页面数99。** 目标 85/99 = 85%。
### 4.3 工厂优先开发顺序
1. `createListPageTests` — 覆盖最多页面(~50 个),第一个开发
2. `renderWithProviders` + `mswServer` — 所有工厂的基础
3. `createDetailPageTests` — 覆盖 ~15 个页面
4. `createFormPageTests` — 覆盖 ~10 个页面
5. `createDashboardPageTests` — 覆盖 ~10 个页面
每个工厂开发后,用第一批中的 2-3 个页面验证设计,确认可行后再推广。
## 5. 验证标准
### 工厂验证
- [ ] 每个工厂至少用 3 个不同页面验证
- [ ] 工厂生成的测试在 CI 中通过
- [ ] `pnpm test:coverage` 输出可查看覆盖率
### 阶段验收
- [ ] 第一批20 个测试文件通过PatientList + AppointmentList 完整覆盖
- [ ] 第二批:累计 86 个测试文件通过
- [ ] 第三批:累计 106 个测试文件通过,页面覆盖率 85%
### 不做
- 不追求 100% 行覆盖率 — 目标是页面覆盖率 85%
- 不测 Ant Design 组件内部行为 — 只测我们的业务逻辑
- 不建立快照测试 — 价值低
- 不测 CSS 样式 — 由视觉回归测试覆盖
## 6. 风险与缓解措施
### 风险 1工厂函数无法覆盖同模式内的变异性
50 个列表页虽然共享 Table + FilterBar 模式,但 API 调用方式、筛选器类型、自定义操作按钮差异较大。如果工厂配置接口不足,会退化为逐个手写。
**缓解:** 开发 ListPage 工厂后,立即用差异最大的 3 个页面PatientList 复杂筛选、AlertList 自定义操作、Users 简单列表)验证。如果配置接口不足,先调整工厂接口再推广。
### 风险 2Ant Design v6 在 jsdom 下的兼容性
antd 的 Modal/Drawer 默认渲染到 `document.body`,超出 testing-library 的默认查询范围。Select/DatePicker 等组件的弹出层在 jsdom 中行为可能不同。
**缓解:**`renderWithProviders` 中设置 `container.baseElement: document.body`;对 Modal 组件使用 `attachTo` 配置;必要时为 antd 弹窗组件编写自定义查询函数。
### 风险 3Zustand v5 测试隔离
Zustand v5 移除了 `<Provider>`store 隔离方式变化。`renderWithProviders` 需要使用 `store.setState()` 直接注入状态而非 Provider 包裹。
**缓解:** 项目已有 Store 测试6 个)验证了 Zustand v5 的测试模式,沿用相同方法。
### 风险 4测试运行时间增长
99 个页面测试 + msw 拦截会显著增加 CI 运行时间。
**缓解:** Vitest 默认并行运行;对慢测试标记 `@slow` 隔离运行CI 中使用 `vitest --reporter=verbose` 配合覆盖率阈值门控(不达标则失败)。
## 附录99 个页面/组件完整分类
### 列表页模式ListPage— 42 个
| # | 页面文件 | 域 |
|---|---------|-----|
| 1 | PatientList | health |
| 2 | AppointmentList | health |
| 3 | DoctorList | health |
| 4 | AlertList | health |
| 5 | AlertRuleList | health |
| 6 | FollowUpTaskList | health |
| 7 | FollowUpRecordList | health |
| 8 | FollowUpTemplateList | health |
| 9 | ConsultationList | health |
| 10 | ArticleManageList | health |
| 11 | PointsProductList | health |
| 12 | PointsOrderList | health |
| 13 | PointsRuleList | health |
| 14 | DeviceManage | health |
| 15 | DialysisManageList | health |
| 16 | OfflineEventList | health |
| 17 | Users | auth |
| 18 | Roles | auth |
| 19 | Organizations | auth |
| 20 | Messages | message |
| 21 | DictionaryManager | config |
| 22 | MenuConfig | config |
| 23 | NumberingRules | config |
| 24 | AuditLogViewer | config |
| 25 | PluginAdmin | plugin |
| 26 | PluginMarket | plugin |
| 27 | PendingTasks | workflow |
| 28 | CompletedTasks | workflow |
| 29 | ProcessDefinitions | workflow |
| 30 | InstanceMonitor | workflow |
| 31 | AiAnalysisList | ai |
| 32 | AiPromptList | ai |
| 33 | ActionInbox | health |
| 34 | StatisticsDashboard | health |
| 35 | ArticleCategoryManage | health |
| 36 | ArticleTagManage | health |
| 37 | PatientTagManage | health |
| 38 | ThemeSettings | config |
| 39 | LanguageManager | config |
| 40 | SystemSettings | config |
| 41 | ChangePassword | auth |
| 42 | MessageTemplates | message |
### 详情页模式DetailPage— 17 个
| # | 页面/组件文件 | 域 |
|---|--------------|-----|
| 1 | PatientDetail | health |
| 2 | ConsultationDetail | health |
| 3 | VitalSignsTab | health/component |
| 4 | LabReportsTab | health/component |
| 5 | FollowUpTab | health/component |
| 6 | FamilyMembersTab | health/component |
| 7 | HealthRecordsTab | health/component |
| 8 | DeviceReadingsTab | health/component |
| 9 | PointsAccountTab | health/component |
| 10 | DailyMonitoringTab | health/component |
| 11 | AiSuggestionTab | health/component |
| 12 | AlertDetailPanel | health/component |
| 13 | ActionDetailDrawer | health/workbench |
| 14 | TaskDetail | health/workbench |
| 15 | TaskQueue | health/workbench |
| 16 | TodoList | health/workbench |
| 17 | TeamOverviewPanel | health/workbench |
### 表单页模式FormPage— 10 个
| # | 页面文件 | 域 |
|---|---------|-----|
| 1 | ArticleEditor | health |
| 2 | Login | auth |
| 3 | DoctorSchedule | health |
| 4 | NotificationPreferences | message |
| 5 | NotificationList | message |
| 6 | DashboardWidgets | dashboard |
| 7 | ProcessViewer | workflow |
| 8 | DoctorWorkbench | health/workbench |
| 9 | OperatorWorkbench | health/workbench |
| 10 | AdminDashboard | health/workbench |
### 仪表盘模式DashboardPage— 10 个
| # | 页面文件 | 域 |
|---|---------|-----|
| 1 | Home | core |
| 2 | AlertDashboard | health |
| 3 | AiUsageDashboard | ai |
| 4 | AiInsightPanel | health/workbench |
| 5 | AdminDashboard (StatisticsDashboard) | health/stats |
| 6 | DoctorDashboard (StatisticsDashboard) | health/stats |
| 7 | HealthDataCenter (StatisticsDashboard) | health/stats |
| 8 | NurseDashboard (StatisticsDashboard) | health/stats |
| 9 | OperatorDashboard (StatisticsDashboard) | health/stats |
| 10 | DashboardWidgets | dashboard |
### 手写测试 — 20 个复杂页面
| # | 页面文件 | 域 | 复杂度原因 |
|---|---------|-----|-----------|
| 1 | PluginCRUDPage | plugin | 动态表单 + 插件数据 |
| 2 | PluginDashboardPage | plugin | 动态图表 |
| 3 | PluginGraphPage | plugin | @xyflow/react 图编辑 |
| 4 | PluginKanbanPage | plugin | 拖拽看板 |
| 5 | PluginTabsPage | plugin | 动态 Tab |
| 6 | PluginTreePage | plugin | 树形结构 |
| 7 | ProcessDesigner | workflow | BPMN 设计器 |
| 8 | CalendarView | health/component | 日历交互 |
| 9 | VitalSignsChart | health/component | ECharts 图表 |
| 10 | ImagePreview | health/component | 图片预览 |
| 11 | PatientSelect | health/component | 异步搜索选择器 |
| 12 | DoctorSelect | health/component | 异步搜索选择器 |
| 13 | FilterBar | component | 通用筛选组件 |
| 14 | ExportButton | component | 文件导出 |
| 15 | StatusTag | health/component | 已有,需补充 |
| 16 | Settings | core | 容器页面 |
| 17 | Workflow | core | 容器页面 |
| 18 | PluginCRUDPageInner | plugin | CRUD 内部逻辑 |
| 19 | DetailDrawer | plugin | 抽屉详情 |
| 20 | ImportModal | plugin | 导入弹窗 |
### 排除(非页面组件)
- `dashboardConstants.tsx` — 常量定义文件,不含 React 组件
> **分类合计:** 42 列表 + 17 详情 + 10 表单 + 10 仪表盘 + 20 手写 = 99。页面覆盖率目标 = (99 - 14 未覆盖) / 99 ≈ 85%。
## 预估工时
| 阶段 | 内容 | 预估人时 | 说明 |
|------|------|---------|------|
| 基础设施 | renderWithProviders + mswServer + testFixtures | 4h | 一次性投入 |
| 工厂开发 | 4 个工厂函数 | 12h | 每个工厂 3h |
| 第一批 | 20 个页面测试 | 12h | 工厂模式,每个 ~0.6h |
| 第二批 | 30 个页面测试 | 18h | 工厂模式 |
| 第三批 | 20 个手写 + 17 个工厂 | 24h | 手写页面耗时较高 |
| **总计** | | **~70h** | 约 2 周全职工作 |