diff --git a/apps/web/src/pages/health/AlertList.test.tsx b/apps/web/src/pages/health/AlertList.test.tsx new file mode 100644 index 0000000..60de833 --- /dev/null +++ b/apps/web/src/pages/health/AlertList.test.tsx @@ -0,0 +1,20 @@ +import { createListPageTests } from '../../test/factories/listPageTests'; +import { createFixtureList, createAlertFixture } from '../../test/fixtures'; +import AlertList from './AlertList'; + +const mockAlerts = createFixtureList(createAlertFixture, 12, [ + { id: 'alert-1', severity: 'high', status: 'active', message: '血压异常偏高' }, + { id: 'alert-2', severity: 'medium', status: 'active', message: '心率偏高' }, +]); + +createListPageTests({ + Component: AlertList, + apiPath: '/api/v1/health/alerts', + columns: ['严重程度', '状态'], + firstRowTexts: ['血压异常偏高'], + totalItems: 12, + hasCreateButton: false, + hasSearch: true, + hasPagination: false, + mockItems: mockAlerts as Record[], +}); diff --git a/apps/web/src/pages/health/DoctorList.test.tsx b/apps/web/src/pages/health/DoctorList.test.tsx new file mode 100644 index 0000000..2f55a3c --- /dev/null +++ b/apps/web/src/pages/health/DoctorList.test.tsx @@ -0,0 +1,18 @@ +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[], +}); diff --git a/apps/web/src/pages/health/PatientList.test.tsx b/apps/web/src/pages/health/PatientList.test.tsx new file mode 100644 index 0000000..5badc4f --- /dev/null +++ b/apps/web/src/pages/health/PatientList.test.tsx @@ -0,0 +1,23 @@ +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, [ + { id: 'patient-1', name: '张三', gender: 'male' }, + { id: 'patient-2', 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[], +}); diff --git a/apps/web/src/test/fixtures/healthFixtures.ts b/apps/web/src/test/fixtures/healthFixtures.ts index ac3f18a..eddaa7c 100644 --- a/apps/web/src/test/fixtures/healthFixtures.ts +++ b/apps/web/src/test/fixtures/healthFixtures.ts @@ -79,7 +79,10 @@ export function createFixtureList( count: number, overridesList: Record[] = [], ): T[] { - return Array.from({ length: count }, (_, i) => factory(overridesList[i] || {})); + return Array.from({ length: count }, (_, i) => { + const autoId = { id: `${i + 1}` }; + return factory({ ...autoId, ...overridesList[i] }); + }); } // --- 分页响应包装 --- diff --git a/apps/web/src/test/setup.ts b/apps/web/src/test/setup.ts index 8ca4762..a688822 100644 --- a/apps/web/src/test/setup.ts +++ b/apps/web/src/test/setup.ts @@ -1,7 +1,36 @@ import '@testing-library/jest-dom'; -import { beforeAll, afterEach, afterAll } from 'vitest'; +import { beforeAll, afterEach, afterAll, vi } from 'vitest'; import { server } from './mocks/server'; +// Ant Design 依赖 window.matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// ResizeObserver mock(Ant Design Table 依赖) +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} as any; + +// Ant Design Modal/Drawer 依赖 getComputedStyle +const originalGetComputedStyle = window.getComputedStyle; +window.getComputedStyle = (elt: Element, pseudoElt?: string | null) => { + const style = originalGetComputedStyle(elt, pseudoElt); + return style; +}; + beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); diff --git a/apps/web/src/test/utils/renderWithProviders.tsx b/apps/web/src/test/utils/renderWithProviders.tsx index 0fc6d3c..1f2c06e 100644 --- a/apps/web/src/test/utils/renderWithProviders.tsx +++ b/apps/web/src/test/utils/renderWithProviders.tsx @@ -3,13 +3,76 @@ import { render, RenderOptions } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; +import { useAuthStore } from '../../stores/auth'; +import { clearApiCache } from '../../api/client'; interface CustomRenderOptions extends Omit { route?: string; + /** 初始化 auth store 状态(默认已认证 + 全权限) */ + authState?: Partial<{ + user: { id: string; username: string; tenant_id: string }; + isAuthenticated: boolean; + permissions: string[]; + }>; } +const ALL_HEALTH_PERMISSIONS = [ + 'health.patient.manage', + 'health.patient.list', + 'health.doctor.manage', + 'health.doctor.list', + 'health.appointment.manage', + 'health.appointment.list', + 'health.alerts.manage', + 'health.alerts.list', + 'health.follow-up.manage', + 'health.follow-up.list', + 'health.follow-up-templates.manage', + 'health.follow-up-templates.list', + 'health.consultation.manage', + 'health.consultation.list', + 'health.dialysis.manage', + 'health.dialysis.list', + 'health.articles.manage', + 'health.articles.list', + 'health.articles.review', + 'health.points.manage', + 'health.points.list', + 'health.health-data.manage', + 'health.health-data.list', + 'ai.analysis.manage', + 'ai.analysis.list', + 'ai.prompt.manage', + 'ai.prompt.list', +]; + +const DEFAULT_AUTH = { + user: { id: 'test-user-id', username: 'admin', tenant_id: 'test-tenant-id' }, + isAuthenticated: true, + permissions: ALL_HEALTH_PERMISSIONS, +}; + +// 简单 JWT payload — 不会过期 +const MOCK_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXVzZXItaWQiLCJleHAiOjk5OTk5OTk5OTl9.mock'; + export function renderWithProviders(ui: ReactElement, options: CustomRenderOptions = {}) { - const { route = '/', ...renderOptions } = options; + const { route = '/', authState, ...renderOptions } = options; + + // 初始化 localStorage token(API client 依赖) + localStorage.setItem('access_token', MOCK_TOKEN); + localStorage.setItem('refresh_token', MOCK_TOKEN); + + // 初始化 Zustand auth store + const auth = { ...DEFAULT_AUTH, ...authState }; + useAuthStore.setState({ + user: auth.user ?? null, + isAuthenticated: auth.isAuthenticated ?? true, + permissions: auth.permissions ?? ['*'], + loading: false, + } as any); + + // 清除 API 缓存 + clearApiCache(); function Wrapper({ children }: { children: React.ReactNode }) { return (