diff --git a/docs/superpowers/plans/2026-05-03-web-page-component-testing-plan.md b/docs/superpowers/plans/2026-05-03-web-page-component-testing-plan.md new file mode 100644 index 0000000..bb3273e --- /dev/null +++ b/docs/superpowers/plans/2026-05-03-web-page-component-testing-plan.md @@ -0,0 +1,1155 @@ +# Web 页面/组件测试 — 第一批实施计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 建立 Web 前端页面测试基础设施(renderWithProviders + msw handlers + testFixtures),开发 ListPage 测试工厂,为第一批 10 个健康管理列表页生成测试文件。 + +**Architecture:** 模式化测试工厂 — createListPageTests(config) 接受页面组件和 API 配置,自动生成「渲染加载 API / 表格列名 / 分页 / 筛选 / 新建按钮 / 编辑按钮」6 个标准测试用例。用 msw 拦截 HTTP 请求,不 mock axios。 + +**Tech Stack:** Vitest 4 + @testing-library/react 16 + msw 2 + Zustand 5 (setState) + +**设计规格:** `docs/superpowers/specs/2026-05-03-web-page-component-testing-design.md` + +--- + +## Chunk 1: 测试基础设施 + +### Task 1: msw handlers 扩展(健康模块 API mock) + +**Files:** +- Create: `apps/web/src/test/mocks/healthHandlers.ts` +- Modify: `apps/web/src/test/mocks/handlers.ts` + +- [ ] **Step 1: 创建 healthHandlers.ts — 患者列表 mock** + +```typescript +// apps/web/src/test/mocks/healthHandlers.ts +import { http, HttpResponse, delay } from 'msw'; + +const DEFAULT_PAGE_SIZE = 20; + +function paginatedResponse(items: T[], total: number, page: number, pageSize = DEFAULT_PAGE_SIZE) { + return HttpResponse.json({ + success: true, + data: { + data: items, + total, + page, + page_size: pageSize, + total_pages: Math.ceil(total / pageSize), + }, + }); +} + +// --- 患者列表 --- +const mockPatients = Array.from({ length: 25 }, (_, i) => ({ + id: `patient-${i + 1}`, + name: `测试患者${i + 1}`, + gender: i % 2 === 0 ? 'male' : 'female', + birth_date: '1990-01-15', + blood_type: 'A', + status: 'active', + verification_status: i < 20 ? 'verified' : 'pending', + source: 'manual', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +export const patientHandlers = [ + http.get('/api/v1/health/patients', async ({ request }) => { + await delay(50); + const url = new URL(request.url); + const page = Number(url.searchParams.get('page') || 1); + const pageSize = Number(url.searchParams.get('page_size') || DEFAULT_PAGE_SIZE); + const start = (page - 1) * pageSize; + const items = mockPatients.slice(start, start + pageSize); + return paginatedResponse(items, mockPatients.length, page, pageSize); + }), + + http.get('/api/v1/health/patients/:id', async ({ params }) => { + await delay(50); + const patient = mockPatients.find((p) => p.id === params.id); + if (!patient) return HttpResponse.json({ success: false, error: 'Not found' }, { status: 404 }); + return HttpResponse.json({ success: true, data: { ...patient, notes: '', allergy_history: '', medical_history_summary: '' } }); + }), +]; +``` + +- [ ] **Step 2: 添加 alert、appointment、doctor handlers** + +```typescript +// 追加到 healthHandlers.ts + +// --- 告警列表 --- +const mockAlerts = Array.from({ length: 12 }, (_, i) => ({ + id: `alert-${i + 1}`, + patient_id: `patient-${i + 1}`, + patient_name: `测试患者${i + 1}`, + alert_type: i % 3 === 0 ? 'vital_sign' : i % 3 === 1 ? 'lab_result' : 'overdue_followup', + severity: (['low', 'medium', 'high'] as const)[i % 3], + status: i < 8 ? 'active' : 'resolved', + message: `告警消息 ${i + 1}`, + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +export const alertHandlers = [ + http.get('/api/v1/health/alerts', async ({ request }) => { + await delay(50); + const url = new URL(request.url); + const page = Number(url.searchParams.get('page') || 1); + const pageSize = Number(url.searchParams.get('page_size') || DEFAULT_PAGE_SIZE); + return paginatedResponse(mockAlerts.slice(0, pageSize), mockAlerts.length, page, pageSize); + }), +]; + +// --- 预约列表 --- +const mockAppointments = Array.from({ length: 15 }, (_, i) => ({ + id: `appt-${i + 1}`, + patient_id: `patient-${i + 1}`, + patient_name: `测试患者${i + 1}`, + doctor_id: `doctor-${(i % 5) + 1}`, + doctor_name: `测试医生${(i % 5) + 1}`, + appointment_date: '2026-05-10', + start_time: '09:00', + end_time: '09:30', + status: (['pending', 'confirmed', 'completed', 'cancelled'] as const)[i % 4], + type: 'follow_up', + notes: '', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +export const appointmentHandlers = [ + http.get('/api/v1/health/appointments', async ({ request }) => { + await delay(50); + const url = new URL(request.url); + const page = Number(url.searchParams.get('page') || 1); + const pageSize = Number(url.searchParams.get('page_size') || DEFAULT_PAGE_SIZE); + return paginatedResponse(mockAppointments.slice(0, pageSize), mockAppointments.length, page, pageSize); + }), +]; + +// --- 医生列表 --- +const mockDoctors = Array.from({ length: 8 }, (_, i) => ({ + id: `doctor-${i + 1}`, + user_id: `user-doc-${i + 1}`, + name: `测试医生${i + 1}`, + department: '内科', + title: '主治医师', + specialization: '心血管内科', + phone: `1380000${String(i + 1).padStart(4, '0')}`, + email: `doctor${i + 1}@test.com`, + status: 'online', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +export const doctorHandlers = [ + http.get('/api/v1/health/doctors', async ({ request }) => { + await delay(50); + const url = new URL(request.url); + const page = Number(url.searchParams.get('page') || 1); + const pageSize = Number(url.searchParams.get('page_size') || DEFAULT_PAGE_SIZE); + return paginatedResponse(mockDoctors.slice(0, pageSize), mockDoctors.length, page, pageSize); + }), +]; +``` + +- [ ] **Step 3: 在 handlers.ts 中注册 health handlers** + +```typescript +// apps/web/src/test/mocks/handlers.ts — 替换末尾 export +import { + patientHandlers, + alertHandlers, + appointmentHandlers, + doctorHandlers, +} from './healthHandlers'; + +export const handlers = [...authHandlers, ...patientHandlers, ...alertHandlers, ...appointmentHandlers, ...doctorHandlers]; +``` + +- [ ] **Step 4: 验证 msw 正常加载** + +Run: `cd apps/web && npx vitest run --reporter=verbose 2>&1 | head -30` +Expected: 现有测试全部通过(msw setup 不影响已有测试) + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/test/mocks/healthHandlers.ts apps/web/src/test/mocks/handlers.ts +git commit -m "test(web): 添加健康模块 msw handlers — 患者告警预约医生 4 组 mock API" +``` + +--- + +### Task 2: testFixtures(测试数据工厂) + +**Files:** +- Create: `apps/web/src/test/fixtures/index.ts` +- Create: `apps/web/src/test/fixtures/healthFixtures.ts` + +- [ ] **Step 1: 创建 healthFixtures.ts — 基础 fixture 工厂函数** + +```typescript +// apps/web/src/test/fixtures/healthFixtures.ts + +// --- 患者 --- +export function createPatientFixture(overrides: Record = {}) { + return { + id: 'patient-1', + name: '张三', + gender: 'male', + birth_date: '1990-01-15', + blood_type: 'A', + status: 'active', + verification_status: 'verified', + source: 'manual', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, + ...overrides, + }; +} + +// --- 医生 --- +export function createDoctorFixture(overrides: Record = {}) { + return { + id: 'doctor-1', + user_id: 'user-doc-1', + name: '李医生', + department: '内科', + title: '主治医师', + specialization: '心血管内科', + phone: '13800000001', + email: 'doctor1@test.com', + status: 'online', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, + ...overrides, + }; +} + +// --- 告警 --- +export function createAlertFixture(overrides: Record = {}) { + return { + id: 'alert-1', + patient_id: 'patient-1', + patient_name: '张三', + alert_type: 'vital_sign', + severity: 'high', + status: 'active', + message: '血压异常偏高', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, + ...overrides, + }; +} + +// --- 预约 --- +export function createAppointmentFixture(overrides: Record = {}) { + return { + id: 'appt-1', + patient_id: 'patient-1', + patient_name: '张三', + doctor_id: 'doctor-1', + doctor_name: '李医生', + appointment_date: '2026-05-10', + start_time: '09:00', + end_time: '09:30', + status: 'pending', + type: 'follow_up', + notes: '', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, + ...overrides, + }; +} + +// --- 批量生成 --- +export function createFixtureList(factory: (overrides?: Record) => T, count: number, overridesList: Record[] = []): T[] { + return Array.from({ length: count }, (_, i) => factory(overridesList[i] || {})); +} + +// --- 分页响应包装 --- +export function wrapPaginated(items: T[], total?: number) { + return { + data: items, + total: total ?? items.length, + page: 1, + page_size: 20, + total_pages: Math.ceil((total ?? items.length) / 20), + }; +} +``` + +- [ ] **Step 2: 创建 fixtures/index.ts — 统一导出** + +```typescript +// apps/web/src/test/fixtures/index.ts +export * from './healthFixtures'; +``` + +- [ ] **Step 3: 在现有 API 测试中验证 fixture** + +Run: `cd apps/web && npx vitest run src/api/health/patients.test.ts --reporter=verbose` +Expected: 现有测试通过(fixture 文件不影响已有代码) + +- [ ] **Step 4: Commit** + +```bash +git add apps/web/src/test/fixtures/ +git commit -m "test(web): 添加测试数据工厂 — healthFixtures + 批量生成 + 分页包装" +``` + +--- + +### Task 3: renderWithProviders(渲染包裹器) + +**Files:** +- Create: `apps/web/src/test/utils/renderWithProviders.tsx` + +- [ ] **Step 1: 编写 renderWithProviders 失败测试** + +```typescript +// apps/web/src/test/utils/renderWithProviders.test.tsx +import { describe, it, expect } from 'vitest'; +import { screen } from '@testing-library/react'; +import { renderWithProviders } from './renderWithProviders'; + +function DummyPage() { + return
Hello Test
; +} + +describe('renderWithProviders', () => { + it('renders child component', () => { + renderWithProviders(); + expect(screen.getByTestId('dummy')).toHaveTextContent('Hello Test'); + }); + + it('wraps with MemoryRouter — Link renders without error', () => { + function PageWithLink() { + return ( +
+ Go +
+ ); + } + renderWithProviders(); + expect(screen.getByText('Go')).toHaveAttribute('href', '/test'); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `cd apps/web && npx vitest run src/test/utils/renderWithProviders.test.tsx` +Expected: FAIL — `renderWithProviders` 不存在 + +- [ ] **Step 3: 实现 renderWithProviders** + +```tsx +// apps/web/src/test/utils/renderWithProviders.tsx +import { ReactElement } from 'react'; +import { render, RenderOptions } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; + +interface CustomRenderOptions extends Omit { + route?: string; +} + +export function renderWithProviders(ui: ReactElement, options: CustomRenderOptions = {}) { + const { route = '/', ...renderOptions } = options; + + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + return render(ui, { wrapper: Wrapper, ...renderOptions }); +} + +// Re-export everything from @testing-library/react for convenience +export * from '@testing-library/react'; +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `cd apps/web && npx vitest run src/test/utils/renderWithProviders.test.tsx` +Expected: PASS — 2 tests passed + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/test/utils/ +git commit -m "test(web): 添加 renderWithProviders — MemoryRouter + AntD ConfigProvider 包裹器" +``` + +## Chunk 2: ListPage 测试工厂 + +### Task 4: createListPageTests 工厂函数 + +**Files:** +- Create: `apps/web/src/test/factories/listPageTests.ts` + +- [ ] **Step 1: 编写工厂接口定义和骨架 — 失败测试** + +```typescript +// apps/web/src/test/factories/listPageTests.test.tsx +import { describe, it, expect } from 'vitest'; +import { createListPageTests } from './listPageTests'; + +describe('createListPageTests', () => { + it('is a function that returns a describe block', () => { + expect(typeof createListPageTests).toBe('function'); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: `cd apps/web && npx vitest run src/test/factories/listPageTests.test.tsx` +Expected: FAIL — 模块不存在 + +- [ ] **Step 3: 实现 createListPageTests 工厂** + +```typescript +// apps/web/src/test/factories/listPageTests.ts +import { describe, it, expect, vi, type Vitest } from 'vitest'; +import { screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '../utils/renderWithProviders'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; +import type { ComponentType } from 'react'; + +export interface ListPageTestConfig { + /** 页面组件 */ + Component: ComponentType; + /** 列表 API 路径(如 '/api/v1/health/patients') */ + apiPath: string; + /** 表格列名数组 — 用于验证表头 */ + columns: string[]; + /** 第一条 mock 数据中会被渲染到表格的文本 */ + firstRowTexts: string[]; + /** mock 数据总条数 */ + totalItems: number; + /** 是否有「新建」按钮 */ + hasCreateButton?: boolean; + /** 新建按钮文本(默认 "新建") */ + createButtonText?: string; + /** 是否有搜索/筛选 */ + hasSearch?: boolean; + /** 是否有分页(默认 true) */ + hasPagination?: boolean; + /** 自定义路由(默认 '/') */ + route?: string; + /** 自定义 mock 数据(默认使用空数组 + totalItems) */ + mockItems?: Record[]; + /** 额外测试用例 */ + extraTests?: () => void; +} + +function createMockResponse(config: ListPageTestConfig) { + const items = config.mockItems ?? []; + return { + success: true, + data: { + data: items, + total: config.totalItems, + page: 1, + page_size: 20, + total_pages: Math.ceil(config.totalItems / 20), + }, + }; +} + +export function createListPageTests(config: ListPageTestConfig) { + const { + Component, + apiPath, + columns, + firstRowTexts, + totalItems, + hasCreateButton = true, + createButtonText = '新建', + hasSearch = true, + hasPagination = true, + route = '/', + extraTests, + } = config; + + describe(`${Component.displayName || Component.name || 'ListPage'}`, () => { + // 每个测试用自定义 handler 覆盖默认 + function setupMock(items?: Record[], total?: number) { + const mockData = items ?? config.mockItems ?? []; + const mockTotal = total ?? totalItems; + server.use( + http.get(apiPath, () => + HttpResponse.json({ + success: true, + data: { data: mockData, total: mockTotal, page: 1, page_size: 20, total_pages: Math.ceil(mockTotal / 20) }, + }), + ), + ); + } + + it('渲染加载状态并显示数据', async () => { + setupMock(config.mockItems); + renderWithProviders(, { route }); + + // 等待加载完成 + await waitFor(() => { + const table = document.querySelector('.ant-table'); + expect(table).toBeInTheDocument(); + }); + }); + + it('表格包含正确的列名', async () => { + setupMock(config.mockItems); + renderWithProviders(, { route }); + + await waitFor(() => { + const headers = document.querySelectorAll('th'); + const headerTexts = Array.from(headers).map((h) => h.textContent?.trim()); + for (const col of columns) { + expect(headerTexts.some((t) => t?.includes(col))).toBe(true); + } + }); + }); + + if (hasPagination && totalItems > 20) { + it('分页器显示正确的总数', async () => { + setupMock(config.mockItems, totalItems); + renderWithProviders(, { route }); + + await waitFor(() => { + const pagination = document.querySelector('.ant-pagination'); + expect(pagination).toBeInTheDocument(); + }); + }); + } + + if (hasCreateButton) { + it('显示新建按钮', async () => { + setupMock(config.mockItems); + renderWithProviders(, { route }); + + await waitFor(() => { + const btn = screen.getByRole('button', { name: new RegExp(createButtonText) }); + expect(btn).toBeInTheDocument(); + }); + }); + } + + if (hasSearch) { + it('搜索/筛选区域存在', async () => { + setupMock(config.mockItems); + renderWithProviders(, { route }); + + await waitFor(() => { + const table = document.querySelector('.ant-table'); + expect(table).toBeInTheDocument(); + }); + // 验证页面至少有一个 input 或 select 用于筛选 + const inputs = document.querySelectorAll('input, .ant-select, .ant-picker'); + expect(inputs.length).toBeGreaterThan(0); + }); + } + + if (extraTests) { + extraTests(); + } + }); +} +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: `cd apps/web && npx vitest run src/test/factories/listPageTests.test.tsx` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/test/factories/ +git commit -m "test(web): 添加 createListPageTests 工厂 — 6 类标准测试用例自动生成" +``` + +--- + +### Task 5: 用 PatientList 验证工厂(完整 CRUD 模式) + +**Files:** +- Create: `apps/web/src/pages/health/PatientList.test.tsx` + +- [ ] **Step 1: 编写 PatientList 测试** + +```typescript +// apps/web/src/pages/health/PatientList.test.tsx +import { http, HttpResponse } from 'msw'; +import { server } from '../../test/mocks/server'; +import { createListPageTests } from '../../test/factories/listPageTests'; +import { createFixtureList, createPatientFixture } from '../../test/fixtures'; +import PatientList from './PatientList'; + +const mockPatients = createFixtureList(createPatientFixture, 25, [ + { name: '张三', gender: 'male' }, + { name: '李四', gender: 'female' }, +]); + +createListPageTests({ + Component: PatientList, + apiPath: '/api/v1/health/patients', + columns: ['患者姓名', '性别', '状态'], + firstRowTexts: ['张三'], + totalItems: 25, + hasCreateButton: true, + createButtonText: '新建', + hasSearch: true, + hasPagination: true, + mockItems: mockPatients as Record[], + extraTests: () => { + it('点击新建按钮打开表单', async () => { + server.use( + http.get('/api/v1/health/patients', () => + HttpResponse.json({ + success: true, + data: { data: mockPatients.slice(0, 20), total: 25, page: 1, page_size: 20, total_pages: 2 }, + }), + ), + ); + const { renderWithProviders } = await import('../../test/utils/renderWithProviders'); + const user = userEvent.setup(); + renderWithProviders(); + + const btn = await screen.findByRole('button', { name: /新建/ }); + await user.click(btn); + + // 抽屉/模态框应该打开 + await waitFor(() => { + const drawer = document.querySelector('.ant-drawer-open, .ant-modal-open'); + expect(drawer).toBeInTheDocument(); + }); + }); + }, +}); + +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +``` + +> **注意:** 此测试验证工厂的完整 CRUD 模式(新建按钮 + 搜索 + 分页 + 抽屉表单)。 + +- [ ] **Step 2: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/PatientList.test.tsx --reporter=verbose` +Expected: PASS — 7 个测试用例(6 标准 + 1 extra) + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/pages/health/PatientList.test.tsx +git commit -m "test(web): PatientList 页面测试 — 验证工厂完整 CRUD 模式" +``` + +--- + +### Task 6: 用 AlertList 验证工厂变体(无创建按钮,有操作按钮) + +**Files:** +- Create: `apps/web/src/pages/health/AlertList.test.tsx` + +- [ ] **Step 1: 编写 AlertList 测试** + +```typescript +// apps/web/src/pages/health/AlertList.test.tsx +import { http, HttpResponse } from 'msw'; +import { server } from '../../test/mocks/server'; +import { createListPageTests } from '../../test/factories/listPageTests'; +import { createFixtureList, createAlertFixture } from '../../test/fixtures'; +import AlertList from './AlertList'; + +const mockAlerts = createFixtureList(createAlertFixture, 12, [ + { severity: 'high', status: 'active', title: '血压异常偏高' }, + { severity: 'medium', status: 'active', title: '心率偏高' }, +]); + +createListPageTests({ + Component: AlertList, + apiPath: '/api/v1/health/alerts', + columns: ['严重程度', '状态'], + firstRowTexts: ['血压异常偏高'], + totalItems: 12, + hasCreateButton: false, + hasSearch: true, + hasPagination: false, + mockItems: mockAlerts as Record[], + extraTests: () => { + it('显示告警严重程度标签', async () => { + server.use( + http.get('/api/v1/health/alerts', () => + HttpResponse.json({ + success: true, + data: { data: mockAlerts, total: 12, page: 1, page_size: 20, total_pages: 1 }, + }), + ), + ); + const { renderWithProviders } = await import('../../test/utils/renderWithProviders'); + renderWithProviders(); + + await waitFor(() => { + const tags = document.querySelectorAll('.ant-tag'); + expect(tags.length).toBeGreaterThan(0); + }); + }); + }, +}); + +import { screen, waitFor } from '@testing-library/react'; +``` + +- [ ] **Step 2: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/AlertList.test.tsx --reporter=verbose` +Expected: PASS — 5 个测试用例(无创建按钮测试,1 extra) + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/pages/health/AlertList.test.tsx +git commit -m "test(web): AlertList 页面测试 — 验证工厂无创建按钮变体" +``` + +--- + +### Task 7: 用 DoctorList 验证简单场景 + +**Files:** +- Create: `apps/web/src/pages/health/DoctorList.test.tsx` + +- [ ] **Step 1: 编写 DoctorList 测试** + +```typescript +// apps/web/src/pages/health/DoctorList.test.tsx +import { http, HttpResponse } from 'msw'; +import { createListPageTests } from '../../test/factories/listPageTests'; +import { createFixtureList, createDoctorFixture } from '../../test/fixtures'; +import DoctorList from './DoctorList'; + +const mockDoctors = createFixtureList(createDoctorFixture, 8); + +createListPageTests({ + Component: DoctorList, + apiPath: '/api/v1/health/doctors', + columns: ['姓名', '科室', '职称'], + firstRowTexts: ['李医生'], + totalItems: 8, + hasCreateButton: true, + createButtonText: '新建', + hasSearch: true, + hasPagination: false, + mockItems: mockDoctors as Record[], +}); +``` + +- [ ] **Step 2: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/DoctorList.test.tsx --reporter=verbose` +Expected: PASS — 4 个标准测试用例 + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/pages/health/DoctorList.test.tsx +git commit -m "test(web): DoctorList 页面测试 — 验证工厂简单列表模式" +``` + +- [ ] **Step 4: 运行全部前端测试确认无回归** + +Run: `cd apps/web && npx vitest run --reporter=verbose` +Expected: 全部通过(原有 36 + 新增 3 页面测试文件) + +- [ ] **Step 5: Commit(如有修复)** + +```bash +git add -u +git commit -m "test(web): 修复工厂验证过程中的兼容性问题" +``` + +## Chunk 3: 第一批列表页测试 + +### Task 8: AppointmentList 测试 + +**Files:** +- Create: `apps/web/src/pages/health/AppointmentList.test.tsx` + +- [ ] **Step 1: 编写 AppointmentList 测试** + +```typescript +// apps/web/src/pages/health/AppointmentList.test.tsx +import { http, HttpResponse } from 'msw'; +import { createListPageTests } from '../../test/factories/listPageTests'; +import { createFixtureList, createAppointmentFixture } from '../../test/fixtures'; +import AppointmentList from './AppointmentList'; + +const mockAppointments = createFixtureList(createAppointmentFixture, 15, [ + { patient_name: '张三', doctor_name: '李医生', status: 'pending' }, + { patient_name: '王五', doctor_name: '赵医生', status: 'confirmed' }, +]); + +createListPageTests({ + Component: AppointmentList, + apiPath: '/api/v1/health/appointments', + columns: ['患者', '医生', '日期', '状态'], + firstRowTexts: ['张三'], + totalItems: 15, + hasCreateButton: true, + hasSearch: true, + hasPagination: false, + mockItems: mockAppointments as Record[], +}); +``` + +- [ ] **Step 2: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/AppointmentList.test.tsx` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add apps/web/src/pages/health/AppointmentList.test.tsx +git commit -m "test(web): AppointmentList 页面测试 — 预约列表模式" +``` + +--- + +### Task 9: FollowUpTaskList + FollowUpRecordList 测试 + +**Files:** +- Create: `apps/web/src/pages/health/FollowUpTaskList.test.tsx` +- Create: `apps/web/src/pages/health/FollowUpRecordList.test.tsx` + +- [ ] **Step 1: 检查 FollowUp API 端点** + +Run: `grep -n "list.*=" apps/web/src/api/health/followUp.ts | head -10` +确认 API 路径,用于配置 `apiPath`。 + +- [ ] **Step 2: 编写 FollowUpTaskList 测试** + +```typescript +// apps/web/src/pages/health/FollowUpTaskList.test.tsx +import { http, HttpResponse } from 'msw'; +import { createListPageTests } from '../../test/factories/listPageTests'; +import FollowUpTaskList from './FollowUpTaskList'; + +// FollowUpTask fixture +const mockTasks = Array.from({ length: 10 }, (_, i) => ({ + id: `task-${i + 1}`, + patient_id: `patient-${i + 1}`, + patient_name: `患者${i + 1}`, + template_id: `tmpl-1`, + template_name: '血压随访', + status: i % 2 === 0 ? 'pending' : 'completed', + due_date: '2026-05-15', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +createListPageTests({ + Component: FollowUpTaskList, + apiPath: '/api/v1/health/follow-up/tasks', + columns: ['患者', '状态'], + firstRowTexts: ['患者1'], + totalItems: 10, + hasCreateButton: false, + hasSearch: true, + hasPagination: false, + mockItems: mockTasks as Record[], +}); +``` + +- [ ] **Step 3: 运行测试并修复 apiPath** + +Run: `cd apps/web && npx vitest run src/pages/health/FollowUpTaskList.test.tsx` +Expected: PASS(如果 API 路径不匹配,根据 Step 1 的结果调整 `apiPath`) + +- [ ] **Step 4: 编写 FollowUpRecordList 测试** + +```typescript +// apps/web/src/pages/health/FollowUpRecordList.test.tsx +import { createListPageTests } from '../../test/factories/listPageTests'; +import FollowUpRecordList from './FollowUpRecordList'; + +const mockRecords = Array.from({ length: 10 }, (_, i) => ({ + id: `record-${i + 1}`, + patient_id: `patient-${i + 1}`, + patient_name: `患者${i + 1}`, + task_id: `task-${i + 1}`, + status: i % 2 === 0 ? 'completed' : 'cancelled', + follow_up_date: '2026-05-10', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +createListPageTests({ + Component: FollowUpRecordList, + apiPath: '/api/v1/health/follow-up/records', + columns: ['患者', '状态'], + firstRowTexts: ['患者1'], + totalItems: 10, + hasCreateButton: false, + hasSearch: true, + hasPagination: false, + mockItems: mockRecords as Record[], +}); +``` + +- [ ] **Step 5: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/FollowUpRecordList.test.tsx` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add apps/web/src/pages/health/FollowUpTaskList.test.tsx apps/web/src/pages/health/FollowUpRecordList.test.tsx +git commit -m "test(web): FollowUpTaskList + FollowUpRecordList 页面测试" +``` + +--- + +### Task 10: ConsultationList 测试 + +**Files:** +- Create: `apps/web/src/pages/health/ConsultationList.test.tsx` + +- [ ] **Step 1: 检查 Consultation API 端点** + +Run: `grep -n "list" apps/web/src/api/health/consultations.ts | head -5` + +- [ ] **Step 2: 编写 ConsultationList 测试** + +```typescript +// apps/web/src/pages/health/ConsultationList.test.tsx +import { createListPageTests } from '../../test/factories/listPageTests'; +import ConsultationList from './ConsultationList'; + +const mockConsultations = Array.from({ length: 8 }, (_, i) => ({ + id: `consult-${i + 1}`, + patient_id: `patient-${i + 1}`, + patient_name: `患者${i + 1}`, + doctor_id: `doctor-1`, + doctor_name: '李医生', + status: (['waiting', 'active', 'closed'] as const)[i % 3], + type: 'online', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +createListPageTests({ + Component: ConsultationList, + apiPath: '/api/v1/health/consultations', + columns: ['患者', '医生', '状态'], + firstRowTexts: ['患者1'], + totalItems: 8, + hasCreateButton: true, + hasSearch: true, + hasPagination: false, + mockItems: mockConsultations as Record[], +}); +``` + +- [ ] **Step 3: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/ConsultationList.test.tsx` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add apps/web/src/pages/health/ConsultationList.test.tsx +git commit -m "test(web): ConsultationList 页面测试 — 咨询列表模式" +``` + +--- + +### Task 11: FollowUpTemplateList 测试 + +**Files:** +- Create: `apps/web/src/pages/health/FollowUpTemplateList.test.tsx` + +- [ ] **Step 1: 检查 FollowUpTemplate API 端点** + +Run: `grep -n "list" apps/web/src/api/health/followUpTemplates.ts | head -5` + +- [ ] **Step 2: 编写 FollowUpTemplateList 测试** + +```typescript +// apps/web/src/pages/health/FollowUpTemplateList.test.tsx +import { createListPageTests } from '../../test/factories/listPageTests'; +import FollowUpTemplateList from './FollowUpTemplateList'; + +const mockTemplates = Array.from({ length: 5 }, (_, i) => ({ + id: `tmpl-${i + 1}`, + name: `随访模板${i + 1}`, + description: `描述${i + 1}`, + category: 'chronic', + frequency_days: 30, + status: 'active', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +createListPageTests({ + Component: FollowUpTemplateList, + apiPath: '/api/v1/health/follow-up/templates', + columns: ['名称', '状态'], + firstRowTexts: ['随访模板1'], + totalItems: 5, + hasCreateButton: true, + hasSearch: true, + hasPagination: false, + mockItems: mockTemplates as Record[], +}); +``` + +- [ ] **Step 3: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/FollowUpTemplateList.test.tsx` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add apps/web/src/pages/health/FollowUpTemplateList.test.tsx +git commit -m "test(web): FollowUpTemplateList 页面测试" +``` + +--- + +### Task 12: DialysisManageList + OfflineEventList 测试 + +**Files:** +- Create: `apps/web/src/pages/health/DialysisManageList.test.tsx` +- Create: `apps/web/src/pages/health/OfflineEventList.test.tsx` + +- [ ] **Step 1: 检查 Dialysis + OfflineEvent API 端点** + +Run: `grep -rn "list" apps/web/src/api/health/dialysis.ts | head -5` +Run: `grep -rn "list" apps/web/src/api/health/actionInbox.ts | head -5` + +- [ ] **Step 2: 编写 DialysisManageList 测试** + +```typescript +// apps/web/src/pages/health/DialysisManageList.test.tsx +import { createListPageTests } from '../../test/factories/listPageTests'; +import DialysisManageList from './DialysisManageList'; + +const mockDialysis = Array.from({ length: 6 }, (_, i) => ({ + id: `dialysis-${i + 1}`, + patient_id: `patient-${i + 1}`, + patient_name: `患者${i + 1}`, + treatment_date: '2026-05-10', + status: (['scheduled', 'in_progress', 'completed'] as const)[i % 3], + dialysis_type: 'hemodialysis', + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +createListPageTests({ + Component: DialysisManageList, + apiPath: '/api/v1/health/dialysis', + columns: ['患者', '状态'], + firstRowTexts: ['患者1'], + totalItems: 6, + hasCreateButton: true, + hasSearch: true, + hasPagination: false, + mockItems: mockDialysis as Record[], +}); +``` + +- [ ] **Step 3: 编写 OfflineEventList 测试** + +```typescript +// apps/web/src/pages/health/OfflineEventList.test.tsx +import { createListPageTests } from '../../test/factories/listPageTests'; +import OfflineEventList from './OfflineEventList'; + +const mockEvents = Array.from({ length: 4 }, (_, i) => ({ + id: `event-${i + 1}`, + title: `线下活动${i + 1}`, + event_date: '2026-05-20', + location: '社区活动中心', + status: 'upcoming', + max_participants: 50, + created_at: '2026-04-01T10:00:00Z', + updated_at: '2026-04-01T10:00:00Z', + version: 1, +})); + +createListPageTests({ + Component: OfflineEventList, + apiPath: '/api/v1/health/offline-events', + columns: ['活动名称', '日期', '状态'], + firstRowTexts: ['线下活动1'], + totalItems: 4, + hasCreateButton: true, + hasSearch: true, + hasPagination: false, + mockItems: mockEvents as Record[], +}); +``` + +- [ ] **Step 4: 运行测试** + +Run: `cd apps/web && npx vitest run src/pages/health/DialysisManageList.test.tsx src/pages/health/OfflineEventList.test.tsx` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/src/pages/health/DialysisManageList.test.tsx apps/web/src/pages/health/OfflineEventList.test.tsx +git commit -m "test(web): DialysisManageList + OfflineEventList 页面测试" +``` + +--- + +### Task 13: 全量验证 + 覆盖率报告 + +- [ ] **Step 1: 运行全部前端测试** + +Run: `cd apps/web && npx vitest run --reporter=verbose` +Expected: 全部通过 + +- [ ] **Step 2: 生成覆盖率报告** + +Run: `cd apps/web && npx vitest run --coverage` +Expected: 页面测试文件从 1 个增加到 10+ 个,覆盖率有所提升 + +- [ ] **Step 3: 最终提交(如有遗漏修复)** + +```bash +git add -u +git commit -m "test(web): 第一批页面测试完成 — 10 个列表页 + 工厂 + 基础设施" +```