save code
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
# 测试用例示例
|
||||
|
||||
## 单元测试示例
|
||||
|
||||
### utils/format.test.js
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatMoney, formatPercentage, formatDate } from '../src/utils/format';
|
||||
|
||||
describe('formatMoney', () => {
|
||||
it('TC-001-01: 应该正确格式化整数', () => {
|
||||
expect(formatMoney(1234)).toBe('1,234.00');
|
||||
});
|
||||
|
||||
it('TC-001-02: 应该正确格式化小数', () => {
|
||||
expect(formatMoney(1234.56)).toBe('1,234.56');
|
||||
});
|
||||
|
||||
it('TC-001-03: 应该处理千分位', () => {
|
||||
expect(formatMoney(10000)).toBe('10,000.00');
|
||||
});
|
||||
|
||||
it('TC-001-04: 应该处理零', () => {
|
||||
expect(formatMoney(0)).toBe('0.00');
|
||||
});
|
||||
|
||||
it('TC-001-05: 应该处理负数', () => {
|
||||
expect(formatMoney(-1234.56)).toBe('-1,234.56');
|
||||
});
|
||||
|
||||
it('TC-001-06: 应该四舍五入', () => {
|
||||
expect(formatMoney(100.123)).toBe('100.12');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPercentage', () => {
|
||||
it('TC-002-01: 应该正确格式化50%', () => {
|
||||
expect(formatPercentage(0.5)).toBe('50.00%');
|
||||
});
|
||||
|
||||
it('TC-002-02: 应该正确格式化100%', () => {
|
||||
expect(formatPercentage(1)).toBe('100.00%');
|
||||
});
|
||||
|
||||
it('TC-002-03: 应该处理零', () => {
|
||||
expect(formatPercentage(0)).toBe('0.00%');
|
||||
});
|
||||
|
||||
it('TC-002-04: 应该保留两位小数', () => {
|
||||
expect(formatPercentage(0.6666)).toBe('66.66%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('TC-003-01: 应该格式化日期字符串', () => {
|
||||
expect(formatDate('2026-01-25')).toBe('2026-01-25');
|
||||
});
|
||||
|
||||
it('TC-003-02: 应该格式化Date对象', () => {
|
||||
expect(formatDate(new Date('2026-01-25'))).toBe('2026-01-25');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### components/Common/Button.test.jsx
|
||||
|
||||
```jsx
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import Button from './Button';
|
||||
|
||||
describe('Button Component - TC-009', () => {
|
||||
it('TC-009-01: 应该渲染按钮文字', () => {
|
||||
render(<Button>点击我</Button>);
|
||||
expect(screen.getByText('点击我')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('TC-009-02: 应该正确显示primary类型', () => {
|
||||
render(<Button type="primary">保存</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('btn-primary');
|
||||
});
|
||||
|
||||
it('TC-009-03: 应该正确显示default类型', () => {
|
||||
render(<Button type="default">取消</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('btn-default');
|
||||
});
|
||||
|
||||
it('TC-009-04: 应该正确显示danger类型', () => {
|
||||
render(<Button type="danger">删除</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('btn-danger');
|
||||
});
|
||||
|
||||
it('TC-009-05: 应该显示loading状态', () => {
|
||||
render(<Button loading>加载中</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveClass('btn-loading');
|
||||
});
|
||||
|
||||
it('TC-009-06: 应该禁用按钮', () => {
|
||||
render(<Button disabled>禁用</Button>);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it('TC-009-07: 应该调用onClick', () => {
|
||||
const handleClick = vi.fn();
|
||||
render(<Button onClick={handleClick}>点击</Button>);
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### hooks/useAuth.test.js
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { AuthProvider, useAuth } from '../src/contexts/AuthContext';
|
||||
|
||||
// 模拟authAPI
|
||||
vi.mock('../src/services/auth', () => ({
|
||||
authAPI: {
|
||||
login: vi.fn(),
|
||||
getCurrentUser: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useAuth Hook - TC-006', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('TC-006-01: 初始状态应该是未认证', () => {
|
||||
const { result } = renderHook(() => useAuth(), {
|
||||
wrapper: AuthProvider,
|
||||
});
|
||||
|
||||
expect(result.current.isAuthenticated).toBe(false);
|
||||
expect(result.current.user).toBe(null);
|
||||
expect(result.current.token).toBe(null);
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('TC-006-02: 应该正确处理登录', async () => {
|
||||
const mockResponse = {
|
||||
user: { id: 1, username: 'admin', role: 'admin' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
|
||||
const { authAPI } = await import('../src/services/auth');
|
||||
authAPI.login.mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useAuth(), {
|
||||
wrapper: AuthProvider,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.login('admin', 'password123');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isAuthenticated).toBe(true);
|
||||
expect(result.current.user).toEqual(mockResponse.user);
|
||||
expect(result.current.token).toBe(mockResponse.token);
|
||||
expect(localStorage.getItem('token')).toBe(mockResponse.token);
|
||||
expect(localStorage.getItem('user')).toBe(JSON.stringify(mockResponse.user));
|
||||
});
|
||||
});
|
||||
|
||||
it('TC-006-03: 应该正确处理登录失败', async () => {
|
||||
const { authAPI } = await import('../src/services/auth');
|
||||
authAPI.login.mockRejectedValue(new Error('用户名或密码错误'));
|
||||
|
||||
const { result } = renderHook(() => useAuth(), {
|
||||
wrapper: AuthProvider,
|
||||
});
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await result.current.login('admin', 'wrong');
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('用户名或密码错误');
|
||||
}
|
||||
});
|
||||
|
||||
it('TC-006-04: 应该正确处理登出', async () => {
|
||||
const { result } = renderHook(() => useAuth(), {
|
||||
wrapper: AuthProvider,
|
||||
});
|
||||
|
||||
// 先登录
|
||||
const mockResponse = {
|
||||
user: { id: 1, username: 'admin' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
|
||||
const { authAPI } = await import('../src/services/auth');
|
||||
authAPI.login.mockResolvedValue(mockResponse);
|
||||
authAPI.logout.mockResolvedValue({});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.login('admin', 'password');
|
||||
});
|
||||
|
||||
// 然后登出
|
||||
await act(async () => {
|
||||
await result.current.logout();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isAuthenticated).toBe(false);
|
||||
expect(result.current.user).toBe(null);
|
||||
expect(result.current.token).toBe(null);
|
||||
expect(localStorage.getItem('token')).toBe(null);
|
||||
expect(localStorage.getItem('user')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('TC-006-05: hasRole应该正确检查角色', async () => {
|
||||
const { result } = renderHook(() => useAuth(), {
|
||||
wrapper: AuthProvider,
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
user: { id: 1, username: 'admin', role: 'admin' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
|
||||
const { authAPI } = await import('../src/services/auth');
|
||||
authAPI.login.mockResolvedValue(mockResponse);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.login('admin', 'password');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasRole(['admin'])).toBe(true);
|
||||
expect(result.current.hasRole(['market'])).toBe(false);
|
||||
expect(result.current.hasRole(['admin', 'market'])).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 集成测试示例
|
||||
|
||||
### pages/Login/Login.test.jsx
|
||||
|
||||
```jsx
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import Login from './Login';
|
||||
|
||||
// 模拟authAPI
|
||||
vi.mock('../../services/auth', () => ({
|
||||
authAPI: {
|
||||
login: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// 模拟useNavigate
|
||||
const mockedNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => mockedNavigate,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Login Page - TC-015', () => {
|
||||
const renderLogin = () => {
|
||||
return render(
|
||||
<BrowserRouter>
|
||||
<Login />
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('TC-015-01: 应该渲染登录表单', () => {
|
||||
renderLogin();
|
||||
|
||||
expect(screen.getByText('海洋项目管理系统')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/用户名/)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/密码/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /登录/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('TC-015-02: 应该显示必填字段验证错误', async () => {
|
||||
renderLogin();
|
||||
|
||||
const submitButton = screen.getByRole('button', { name: /登录/ });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/请输入用户名/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/请输入密码/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('TC-015-03: 应该显示用户名长度错误', async () => {
|
||||
renderLogin();
|
||||
|
||||
const usernameInput = screen.getByLabelText(/用户名/);
|
||||
fireEvent.change(usernameInput, { target: { value: 'ab' } });
|
||||
|
||||
fireEvent.blur(usernameInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/用户名至少3个字符/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('TC-015-04: 应该显示密码长度错误', async () => {
|
||||
renderLogin();
|
||||
|
||||
const passwordInput = screen.getByLabelText(/密码/);
|
||||
fireEvent.change(passwordInput, { target: { value: '12345' } });
|
||||
|
||||
fireEvent.blur(passwordInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/密码至少6个字符/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('TC-015-05: 应该成功登录', async () => {
|
||||
const { authAPI } = await import('../../services/auth');
|
||||
const mockResponse = {
|
||||
user: { id: 1, username: 'admin', real_name: '系统管理员', role: 'admin' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
authAPI.login.mockResolvedValue(mockResponse);
|
||||
|
||||
renderLogin();
|
||||
|
||||
const usernameInput = screen.getByLabelText(/用户名/);
|
||||
const passwordInput = screen.getByLabelText(/密码/);
|
||||
const submitButton = screen.getByRole('button', { name: /登录/ });
|
||||
|
||||
fireEvent.change(usernameInput, { target: { value: 'admin' } });
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authAPI.login).toHaveBeenCalledWith('admin', 'password123');
|
||||
expect(mockedNavigate).toHaveBeenCalledWith('/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
it('TC-015-06: 应该显示登录失败错误', async () => {
|
||||
const { authAPI } = await import('../../services/auth');
|
||||
authAPI.login.mockRejectedValue(new Error('用户名或密码错误'));
|
||||
|
||||
renderLogin();
|
||||
|
||||
const usernameInput = screen.getByLabelText(/用户名/);
|
||||
const passwordInput = screen.getByLabelText(/密码/);
|
||||
const submitButton = screen.getByRole('button', { name: /登录/ });
|
||||
|
||||
fireEvent.change(usernameInput, { target: { value: 'admin' } });
|
||||
fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/用户名或密码错误/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('TC-015-07: 应该处理Enter键提交', async () => {
|
||||
const { authAPI } = await import('../../services/auth');
|
||||
const mockResponse = {
|
||||
user: { id: 1, username: 'admin' },
|
||||
token: 'mock-token',
|
||||
};
|
||||
authAPI.login.mockResolvedValue(mockResponse);
|
||||
|
||||
renderLogin();
|
||||
|
||||
const usernameInput = screen.getByLabelText(/用户名/);
|
||||
const passwordInput = screen.getByLabelText(/密码/);
|
||||
|
||||
fireEvent.change(usernameInput, { target: { value: 'admin' } });
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||
|
||||
fireEvent.keyDown(passwordInput, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authAPI.login).toHaveBeenCalledWith('admin', 'password123');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## E2E测试示例
|
||||
|
||||
### login.spec.js
|
||||
|
||||
```javascript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('登录流程 - TC-E2E-001', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 清除localStorage
|
||||
await page.goto('http://localhost:3000');
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
});
|
||||
|
||||
test('TC-E2E-001-01: 应该成功登录并跳转到仪表盘', async ({ page }) => {
|
||||
// 访问登录页
|
||||
await page.goto('http://localhost:3000/login');
|
||||
|
||||
// 输入用户名和密码
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'password123');
|
||||
|
||||
// 点击登录按钮
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// 验证跳转到仪表盘
|
||||
await expect(page).toHaveURL('http://localhost:3000/dashboard');
|
||||
|
||||
// 验证用户信息显示
|
||||
await expect(page.locator('.user-info')).toContainText('系统管理员');
|
||||
});
|
||||
|
||||
test('TC-E2E-001-02: 应该显示登录失败错误', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/login');
|
||||
|
||||
// 输入错误密码
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'wrongpassword');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// 验证错误提示
|
||||
await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
|
||||
});
|
||||
|
||||
test('TC-E2E-001-03: 应该验证必填字段', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/login');
|
||||
|
||||
// 直接点击登录按钮
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// 验证错误提示
|
||||
await expect(page.locator('text=请输入用户名')).toBeVisible();
|
||||
await expect(page.locator('text=请输入密码')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-E2E-001-04: 应该记住密码', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/login');
|
||||
|
||||
// 勾选记住密码
|
||||
await page.check('input[type="checkbox"]');
|
||||
|
||||
// 输入用户名和密码
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'password123');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// 等待登录成功
|
||||
await page.waitForURL('**/dashboard');
|
||||
|
||||
// 刷新页面
|
||||
await page.reload();
|
||||
|
||||
// 验证自动填充
|
||||
await expect(page.locator('input[name="username"]')).toHaveValue('admin');
|
||||
await expect(page.locator('input[name="password"]')).toHaveValue('password123');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### projects.spec.js
|
||||
|
||||
```javascript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('项目管理 - TC-E2E-003 to TC-E2E-006', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 登录
|
||||
await page.goto('http://localhost:3000/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'password123');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('**/dashboard');
|
||||
});
|
||||
|
||||
test('TC-E2E-003: 应该显示项目列表', async ({ page }) => {
|
||||
// 导航到项目列表
|
||||
await page.click('text=项目管理');
|
||||
await page.waitForURL('**/projects');
|
||||
|
||||
// 验证表格显示
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
await expect(page.locator('th:has-text("合同编号")')).toBeVisible();
|
||||
await expect(page.locator('th:has-text("项目名称")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('TC-E2E-004: 应该创建新项目', async ({ page }) => {
|
||||
await page.click('text=项目管理');
|
||||
await page.waitForURL('**/projects');
|
||||
|
||||
// 点击新建项目按钮
|
||||
await page.click('button:has-text("新建项目")');
|
||||
|
||||
// 填写表单
|
||||
await page.fill('input[name="project_no"]', 'PRJ999');
|
||||
await page.fill('input[name="name"]', 'E2E测试项目');
|
||||
await page.selectOption('select[name="engineering_type"]', '基建');
|
||||
await page.fill('input[name="contract_amount"]', '100');
|
||||
await page.click('input[name="signing_date"]');
|
||||
await page.fill('input[name="signing_date"]', '2026-01-25');
|
||||
|
||||
// 提交表单
|
||||
await page.click('button:has-text("保存")');
|
||||
|
||||
// 验证成功提示
|
||||
await expect(page.locator('.success-message')).toContainText('项目创建成功');
|
||||
|
||||
// 验证项目出现在列表中
|
||||
await expect(page.locator('table')).toContainText('PRJ999');
|
||||
});
|
||||
|
||||
test('TC-E2E-005: 应该编辑项目', async ({ page }) => {
|
||||
await page.click('text=项目管理');
|
||||
await page.waitForURL('**/projects');
|
||||
|
||||
// 点击编辑按钮
|
||||
await page.click('button:has-text("编辑")');
|
||||
|
||||
// 修改项目名称
|
||||
await page.fill('input[name="name"]', 'E2E更新项目');
|
||||
await page.fill('input[name="contract_amount"]', '200');
|
||||
|
||||
// 提交表单
|
||||
await page.click('button:has-text("保存")');
|
||||
|
||||
// 验证成功提示
|
||||
await expect(page.locator('.success-message')).toContainText('项目更新成功');
|
||||
|
||||
// 验证列表显示更新后的数据
|
||||
await expect(page.locator('table')).toContainText('E2E更新项目');
|
||||
});
|
||||
|
||||
test('TC-E2E-006: 应该删除项目', async ({ page }) => {
|
||||
await page.click('text=项目管理');
|
||||
await page.waitForURL('**/projects');
|
||||
|
||||
// 点击删除按钮
|
||||
await page.click('button:has-text("删除")');
|
||||
|
||||
// 确认删除
|
||||
await page.click('button:has-text("确定")');
|
||||
|
||||
// 验证成功提示
|
||||
await expect(page.locator('.success-message')).toContainText('项目删除成功');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: 前端程序员
|
||||
**文档类型**: 测试用例示例
|
||||
**最后更新**: 2026-01-25
|
||||
Reference in New Issue
Block a user