Files
hms/docs/superpowers/plans/2026-05-03-web-page-component-testing-plan.md
iven 8f7f75ac25
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(plan): 页面/组件测试第一批实施计划 — 3 Chunk 13 Task 1155 行
- Chunk 1: 测试基础设施(msw health handlers + fixtures + renderWithProviders)
- Chunk 2: ListPage 测试工厂(createListPageTests + 3 页面验证)
- Chunk 3: 第一批 7 个列表页测试(预约/随访/咨询/透析/活动)
2026-05-03 22:58:51 +08:00

1156 lines
34 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 页面/组件测试 — 第一批实施计划
> **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<T>(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<string, unknown> = {}) {
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<string, unknown> = {}) {
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<string, unknown> = {}) {
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<string, unknown> = {}) {
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<T>(factory: (overrides?: Record<string, unknown>) => T, count: number, overridesList: Record<string, unknown>[] = []): T[] {
return Array.from({ length: count }, (_, i) => factory(overridesList[i] || {}));
}
// --- 分页响应包装 ---
export function wrapPaginated<T>(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 <div data-testid="dummy">Hello Test</div>;
}
describe('renderWithProviders', () => {
it('renders child component', () => {
renderWithProviders(<DummyPage />);
expect(screen.getByTestId('dummy')).toHaveTextContent('Hello Test');
});
it('wraps with MemoryRouter — Link renders without error', () => {
function PageWithLink() {
return (
<div>
<a href="/test">Go</a>
</div>
);
}
renderWithProviders(<PageWithLink />);
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<RenderOptions, 'wrapper'> {
route?: string;
}
export function renderWithProviders(ui: ReactElement, options: CustomRenderOptions = {}) {
const { route = '/', ...renderOptions } = options;
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<MemoryRouter initialEntries={[route]}>
<ConfigProvider locale={zhCN}>{children}</ConfigProvider>
</MemoryRouter>
);
}
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<string, unknown>[];
/** 额外测试用例 */
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<string, unknown>[], 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(<Component />, { route });
// 等待加载完成
await waitFor(() => {
const table = document.querySelector('.ant-table');
expect(table).toBeInTheDocument();
});
});
it('表格包含正确的列名', async () => {
setupMock(config.mockItems);
renderWithProviders(<Component />, { 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(<Component />, { route });
await waitFor(() => {
const pagination = document.querySelector('.ant-pagination');
expect(pagination).toBeInTheDocument();
});
});
}
if (hasCreateButton) {
it('显示新建按钮', async () => {
setupMock(config.mockItems);
renderWithProviders(<Component />, { route });
await waitFor(() => {
const btn = screen.getByRole('button', { name: new RegExp(createButtonText) });
expect(btn).toBeInTheDocument();
});
});
}
if (hasSearch) {
it('搜索/筛选区域存在', async () => {
setupMock(config.mockItems);
renderWithProviders(<Component />, { 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<string, unknown>[],
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(<PatientList />);
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<string, unknown>[],
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(<AlertList />);
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<string, unknown>[],
});
```
- [ ] **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<string, unknown>[],
});
```
- [ ] **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<string, unknown>[],
});
```
- [ ] **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<string, unknown>[],
});
```
- [ ] **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<string, unknown>[],
});
```
- [ ] **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<string, unknown>[],
});
```
- [ ] **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<string, unknown>[],
});
```
- [ ] **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<string, unknown>[],
});
```
- [ ] **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 个列表页 + 工厂 + 基础设施"
```