# 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[]; } function createListPageTests(config: ListPageTestConfig): void; // 详情页测试工厂 interface DetailPageTestConfig { component: React.ComponentType; detailApiUrl: string; tabs: Array<{ label: string; apiUrl: string }>; mockDetail: Record; } function createDetailPageTests(config: DetailPageTestConfig): void; // 表单页测试工厂 interface FormPageTestConfig { component: React.ComponentType; submitApiUrl: string; submitMethod: 'POST' | 'PUT'; requiredFields: string[]; mockInitialValues?: Record; } 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; userRole?: 'admin' | 'doctor' | 'nurse'; } ): { ...RenderResult; store: ReturnType; }; ``` 包裹内容: - `` — 模拟路由 - Zustand store 重置 + 预填充 — Zustand v5 不使用 Provider,通过 `zustand/testing` 或直接 store.setState 注入测试状态 - Ant Design `` — 中文 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 简单列表)验证。如果配置接口不足,先调整工厂接口再推广。 ### 风险 2:Ant Design v6 在 jsdom 下的兼容性 antd 的 Modal/Drawer 默认渲染到 `document.body`,超出 testing-library 的默认查询范围。Select/DatePicker 等组件的弹出层在 jsdom 中行为可能不同。 **缓解:** 在 `renderWithProviders` 中设置 `container.baseElement: document.body`;对 Modal 组件使用 `attachTo` 配置;必要时为 antd 弹窗组件编写自定义查询函数。 ### 风险 3:Zustand v5 测试隔离 Zustand v5 移除了 ``,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 周全职工作 |