test(web): 添加 vitest 单元测试基础设施和初始测试用例
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

- 安装 vitest + @testing-library/react + @testing-library/jest-dom + jsdom
- 创建 vitest.config.ts (jsdom 环境, 全局 API, e2e 目录排除)
- 创建 test/setup.ts (@testing-library/jest-dom 匹配器)
- 添加 29 个测试用例: health 常量 (14), useThemeMode hook (2), StatusTag 组件 (13)
This commit is contained in:
iven
2026-04-25 10:11:30 +08:00
parent 945ccd64ba
commit 55a3fd32d0
7 changed files with 965 additions and 54 deletions

View File

@@ -8,6 +8,7 @@
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui" "test:e2e:ui": "playwright test --ui"
}, },
@@ -29,6 +30,8 @@
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^24.12.2", "@types/node": "^24.12.2",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -37,9 +40,11 @@
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0", "globals": "^17.4.0",
"jsdom": "^29.0.2",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"typescript": "~6.0.2", "typescript": "~6.0.2",
"typescript-eslint": "^8.58.0", "typescript-eslint": "^8.58.0",
"vite": "^8.0.4" "vite": "^8.0.4",
"vitest": "^4.1.5"
} }
} }

773
apps/web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
import { describe, it, expect } from 'vitest'
import {
GENDER_OPTIONS,
BLOOD_TYPE_OPTIONS,
STATUS_OPTIONS,
} from './health'
describe('GENDER_OPTIONS', () => {
it('should be an array with 3 items', () => {
expect(GENDER_OPTIONS).toBeInstanceOf(Array)
expect(GENDER_OPTIONS).toHaveLength(3)
})
it('each item should have value and label string properties', () => {
for (const opt of GENDER_OPTIONS) {
expect(opt).toHaveProperty('value')
expect(opt).toHaveProperty('label')
expect(typeof opt.value).toBe('string')
expect(typeof opt.label).toBe('string')
}
})
it('should contain expected gender values', () => {
const values = GENDER_OPTIONS.map((o) => o.value)
expect(values).toEqual(['male', 'female', 'other'])
})
it('should contain expected Chinese labels', () => {
const labels = GENDER_OPTIONS.map((o) => o.label)
expect(labels).toEqual(['男', '女', '其他'])
})
})
describe('BLOOD_TYPE_OPTIONS', () => {
it('should be an array with 4 items', () => {
expect(BLOOD_TYPE_OPTIONS).toBeInstanceOf(Array)
expect(BLOOD_TYPE_OPTIONS).toHaveLength(4)
})
it('each item should have value and label string properties', () => {
for (const opt of BLOOD_TYPE_OPTIONS) {
expect(opt).toHaveProperty('value')
expect(opt).toHaveProperty('label')
expect(typeof opt.value).toBe('string')
expect(typeof opt.label).toBe('string')
}
})
it('should contain expected blood type values', () => {
const values = BLOOD_TYPE_OPTIONS.map((o) => o.value)
expect(values).toEqual(['A', 'B', 'AB', 'O'])
})
it('should contain expected Chinese labels', () => {
const labels = BLOOD_TYPE_OPTIONS.map((o) => o.label)
expect(labels).toEqual(['A 型', 'B 型', 'AB 型', 'O 型'])
})
})
describe('STATUS_OPTIONS', () => {
it('should be an array with 4 items', () => {
expect(STATUS_OPTIONS).toBeInstanceOf(Array)
expect(STATUS_OPTIONS).toHaveLength(4)
})
it('each item should have value and label string properties', () => {
for (const opt of STATUS_OPTIONS) {
expect(opt).toHaveProperty('value')
expect(opt).toHaveProperty('label')
expect(typeof opt.value).toBe('string')
expect(typeof opt.label).toBe('string')
}
})
it('should include an empty-string value for "all statuses" filter', () => {
const allOption = STATUS_OPTIONS.find((o) => o.value === '')
expect(allOption).toBeDefined()
expect(allOption!.label).toBe('全部状态')
})
it('should contain expected status values', () => {
const values = STATUS_OPTIONS.map((o) => o.value)
expect(values).toEqual(['', 'active', 'inactive', 'deceased'])
})
})

View File

@@ -0,0 +1,15 @@
import { describe, it, expect } from 'vitest'
import { renderHook } from '@testing-library/react'
import { useThemeMode } from './useThemeMode'
describe('useThemeMode', () => {
it('should return false when no ConfigProvider is present (light default)', () => {
const { result } = renderHook(() => useThemeMode())
expect(result.current).toBe(false)
})
it('should return a boolean value', () => {
const { result } = renderHook(() => useThemeMode())
expect(typeof result.current).toBe('boolean')
})
})

View File

@@ -0,0 +1,120 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { StatusTag } from './StatusTag'
describe('StatusTag', () => {
describe('appointment statuses', () => {
it('renders pending status with gold color', () => {
const { container } = render(<StatusTag status="pending" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toBeInTheDocument()
expect(tag).toHaveClass('ant-tag-gold')
expect(screen.getByText('待确认')).toBeInTheDocument()
})
it('renders confirmed status with blue color', () => {
const { container } = render(<StatusTag status="confirmed" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-blue')
expect(screen.getByText('已确认')).toBeInTheDocument()
})
it('renders completed status with green color', () => {
const { container } = render(<StatusTag status="completed" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-green')
expect(screen.getByText('已完成')).toBeInTheDocument()
})
it('renders cancelled status with default color', () => {
const { container } = render(<StatusTag status="cancelled" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toBeInTheDocument()
expect(screen.getByText('已取消')).toBeInTheDocument()
})
it('renders no_show status with red color', () => {
const { container } = render(<StatusTag status="no_show" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-red')
expect(screen.getByText('未到诊')).toBeInTheDocument()
})
})
describe('follow-up statuses', () => {
it('renders overdue status with red color', () => {
const { container } = render(<StatusTag status="overdue" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-red')
expect(screen.getByText('逾期')).toBeInTheDocument()
})
it('renders in_progress status with processing color', () => {
const { container } = render(<StatusTag status="in_progress" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-processing')
expect(screen.getByText('进行中')).toBeInTheDocument()
})
})
describe('consultation statuses', () => {
it('renders waiting status with gold color', () => {
const { container } = render(<StatusTag status="waiting" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-gold')
expect(screen.getByText('等待中')).toBeInTheDocument()
})
it('renders active consultation status with green color', () => {
const { container } = render(<StatusTag status="active" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-green')
expect(screen.getByText('进行中')).toBeInTheDocument()
})
it('renders closed status with default color', () => {
const { container } = render(<StatusTag status="closed" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toBeInTheDocument()
expect(screen.getByText('已关闭')).toBeInTheDocument()
})
})
describe('patient statuses', () => {
it('renders inactive status with default color', () => {
const { container } = render(<StatusTag status="inactive" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toBeInTheDocument()
expect(screen.getByText('停用')).toBeInTheDocument()
})
it('renders deceased status with default color', () => {
const { container } = render(<StatusTag status="deceased" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toBeInTheDocument()
expect(screen.getByText('已故')).toBeInTheDocument()
})
it('renders verified status with green color', () => {
const { container } = render(<StatusTag status="verified" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toHaveClass('ant-tag-green')
expect(screen.getByText('已认证')).toBeInTheDocument()
})
})
describe('unknown status fallback', () => {
it('renders unknown status text with default color', () => {
const { container } = render(<StatusTag status="unknown_status" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toBeInTheDocument()
expect(screen.getByText('unknown_status')).toBeInTheDocument()
})
it('renders empty string status with default color', () => {
const { container } = render(<StatusTag status="" />)
const tag = container.querySelector('.ant-tag')
expect(tag).toBeInTheDocument()
})
})
})

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom'

18
apps/web/vitest.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
exclude: ['e2e/**', 'node_modules/**'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})