import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { RoutesPage } from '@/client/admin/pages/Routes';
import { TestWrapper } from '~/utils/client/test-render';
// Import mocked modules
import { routeClient } from '@/client/api';
// Mock next-themes 组件
vi.mock('next-themes', () => ({
ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
useTheme: () => ({
theme: 'light',
setTheme: vi.fn(),
}),
}));
// Mock activityClient for ActivitySelect component
vi.mock('@/client/api', () => ({
activityClient: {
$get: vi.fn().mockResolvedValue({
status: 200,
ok: true,
json: async () => ({
data: [
{
id: 1,
name: '北京去程活动',
type: 'departure'
},
{
id: 2,
name: '上海返程活动',
type: 'return'
}
],
pagination: {
total: 2,
current: 1,
pageSize: 50
}
})
})
},
routeClient: {
$get: vi.fn().mockResolvedValue({
status: 200,
ok: true,
json: async () => ({
data: [
{
id: 1,
name: '北京到上海路线',
startPoint: '北京',
endPoint: '上海',
pickupPoint: '北京西站',
dropoffPoint: '上海南站',
departureTime: '2025-10-17T08:00:00.000Z',
vehicleType: 'bus',
price: 200,
seatCount: 40,
availableSeats: 40,
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
activity: {
id: 1,
name: '北京去程活动',
type: 'departure'
}
},
{
id: 2,
name: '上海到北京路线',
startPoint: '上海',
endPoint: '北京',
pickupPoint: '上海南站',
dropoffPoint: '北京西站',
departureTime: '2025-10-17T16:00:00.000Z',
vehicleType: 'van',
price: 150,
seatCount: 20,
availableSeats: 20,
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
activity: {
id: 2,
name: '上海返程活动',
type: 'return'
}
}
],
pagination: {
total: 2,
current: 1,
pageSize: 20
}
})
}),
$post: vi.fn().mockResolvedValue({
status: 201,
ok: true,
json: async () => ({
id: 3,
name: '新建路线',
startPoint: '广州',
endPoint: '深圳',
pickupPoint: '广州东站',
dropoffPoint: '深圳北站',
departureTime: '2025-10-18T08:00:00.000Z',
vehicleType: 'bus',
price: 100,
seatCount: 40,
availableSeats: 40,
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z'
})
}),
':id': {
$put: vi.fn().mockResolvedValue({
status: 200,
ok: true,
json: async () => ({
id: 1,
name: '更新后的路线',
startPoint: '北京',
endPoint: '上海',
pickupPoint: '北京西站',
dropoffPoint: '上海南站',
departureTime: '2025-10-17T08:00:00.000Z',
vehicleType: 'bus',
price: 250,
seatCount: 40,
availableSeats: 40,
isDisabled: 0
})
}),
$delete: vi.fn().mockResolvedValue({
status: 204,
ok: true
})
}
}
}));
describe('RoutesPage 集成测试', () => {
const user = userEvent.setup();
beforeEach(() => {
vi.clearAllMocks();
// 模拟缺失的 Pointer Events API
if (!Element.prototype.hasPointerCapture) {
Element.prototype.hasPointerCapture = vi.fn(() => false);
}
if (!Element.prototype.releasePointerCapture) {
Element.prototype.releasePointerCapture = vi.fn();
}
});
it('应该正确渲染路线管理页面标题', async () => {
render(
);
expect(screen.getByText('路线管理')).toBeInTheDocument();
expect(screen.getByText('新建路线')).toBeInTheDocument();
});
it('应该显示路线列表和搜索功能', async () => {
render(
);
// 等待数据加载
await waitFor(() => {
expect(screen.getByPlaceholderText('搜索路线名称、地点或车型...')).toBeInTheDocument();
});
expect(screen.getByText('路线列表')).toBeInTheDocument();
// 等待数据正确加载 - 增加超时和更宽松的条件
await waitFor(() => {
const countText = screen.getByText(/当前共有 \d+ 条路线/);
expect(countText).toBeInTheDocument();
}, { timeout: 5000 });
});
it('应该处理搜索功能', async () => {
render(
);
const searchInput = screen.getByPlaceholderText('搜索路线名称、地点或车型...');
// 输入搜索关键词
await user.type(searchInput, '北京');
// 等待防抖搜索生效
await waitFor(() => {
expect(searchInput).toHaveValue('北京');
});
});
it('应该显示车型筛选功能', async () => {
const user = userEvent.setup();
render(
);
// 等待数据加载
await waitFor(() => {
expect(screen.getByText('路线列表')).toBeInTheDocument();
});
// 验证车型筛选器存在
const vehicleTypeFilter = screen.getByTestId('route-vehicle-type-filter');
expect(vehicleTypeFilter).toBeInTheDocument();
// 点击Select来展开选项
await user.click(vehicleTypeFilter);
// 验证筛选选项存在
expect(screen.getByText('大巴')).toBeInTheDocument();
expect(screen.getByText('中巴')).toBeInTheDocument();
expect(screen.getByText('小车')).toBeInTheDocument();
});
it('应该显示创建路线按钮并打开模态框', async () => {
render(
);
// 等待数据加载
await waitFor(() => {
expect(screen.getByText('新建路线')).toBeInTheDocument();
});
const createButton = screen.getByRole('button', { name: /新建路线/i });
await user.click(createButton);
// 验证模态框标题
expect(screen.getByRole('heading', { name: '创建路线' })).toBeInTheDocument();
});
it('应该显示分页组件', async () => {
render(
);
// 验证分页控件存在 - 等待数据加载
await waitFor(() => {
expect(screen.getByText('当前共有 2 条路线')).toBeInTheDocument();
});
// 验证分页信息存在
expect(screen.getByText('当前共有 2 条路线')).toBeInTheDocument();
});
it('应该处理表格数据加载状态', async () => {
render(
);
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
expect(screen.getByText('上海到北京路线')).toBeInTheDocument();
});
});
it('应该显示正确的表格列标题', async () => {
render(
);
// 等待数据加载
await waitFor(() => {
expect(screen.getByText('路线名称')).toBeInTheDocument();
expect(screen.getByText('出发地')).toBeInTheDocument();
expect(screen.getByText('目的地')).toBeInTheDocument();
expect(screen.getByText('车型')).toBeInTheDocument();
expect(screen.getByText('价格')).toBeInTheDocument();
expect(screen.getByText('可用座位')).toBeInTheDocument();
expect(screen.getByText('出发时间')).toBeInTheDocument();
expect(screen.getByText('状态')).toBeInTheDocument();
expect(screen.getByText('操作')).toBeInTheDocument();
});
});
it('应该显示路线数据在表格中', async () => {
render(
);
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
expect(screen.getByText('上海到北京路线')).toBeInTheDocument();
expect(screen.getAllByText('北京').length).toBeGreaterThan(0);
expect(screen.getAllByText('上海').length).toBeGreaterThan(0);
expect(screen.getByText('bus')).toBeInTheDocument();
expect(screen.getByText('van')).toBeInTheDocument();
expect(screen.getByText('¥200')).toBeInTheDocument();
expect(screen.getByText('¥150')).toBeInTheDocument();
expect(screen.getAllByText('启用').length).toBeGreaterThan(0);
});
});
it('应该包含启用/禁用、编辑和删除操作按钮', async () => {
render(
);
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 查找操作按钮
const actionButtons = screen.getAllByRole('button');
const hasActionButtons = actionButtons.some(button =>
button.textContent?.includes('禁用') ||
button.textContent?.includes('启用') ||
button.textContent?.includes('编辑') ||
button.innerHTML.includes('edit') ||
button.innerHTML.includes('trash')
);
expect(hasActionButtons).toBe(true);
});
it('应该处理创建路线表单提交成功', async () => {
const user = userEvent.setup();
render(
);
// 等待数据加载
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 打开创建路线模态框
const createButton = screen.getByRole('button', { name: /新建路线/i });
await user.click(createButton);
// 验证模态框显示
expect(screen.getByRole('heading', { name: '创建路线' })).toBeInTheDocument();
// 验证表单字段存在 - 使用data-testid
expect(screen.getByTestId('route-name-input')).toBeInTheDocument();
expect(screen.getByTestId('start-point-input')).toBeInTheDocument();
expect(screen.getByTestId('end-point-input')).toBeInTheDocument();
expect(screen.getByTestId('pickup-point-input')).toBeInTheDocument();
expect(screen.getByTestId('dropoff-point-input')).toBeInTheDocument();
expect(screen.getByTestId('departure-time-input')).toBeInTheDocument();
expect(screen.getByTestId('price-input')).toBeInTheDocument();
expect(screen.getByTestId('seat-count-input')).toBeInTheDocument();
expect(screen.getByTestId('available-seats-input')).toBeInTheDocument();
// 验证ActivitySelect组件存在并测试点击交互
const activitySelect = screen.getByTestId('activity-select');
expect(activitySelect).toBeInTheDocument();
// 点击ActivitySelect来展开选项
await user.click(activitySelect);
// 等待活动选项加载并验证选项存在
await waitFor(() => {
expect(screen.getByText('北京去程活动 (去程)')).toBeInTheDocument();
expect(screen.getByText('上海返程活动 (返程)')).toBeInTheDocument();
});
});
it('应该处理启用/禁用路线操作', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 查找启用/禁用按钮
const toggleButtons = screen.getAllByRole('button').filter(btn =>
btn.textContent?.includes('禁用') || btn.textContent?.includes('启用')
);
if (toggleButtons.length > 0) {
// 模拟确认对话框
window.confirm = vi.fn().mockReturnValue(true);
await user.click(toggleButtons[0]);
// 验证确认对话框被调用
expect(window.confirm).toHaveBeenCalledWith('确定要禁用这条路线吗?');
}
});
it('应该处理删除路线操作', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 查找删除按钮
const deleteButtons = screen.getAllByRole('button').filter(btn =>
btn.innerHTML.includes('trash') || btn.getAttribute('aria-label')?.includes('delete')
);
if (deleteButtons.length > 0) {
// 模拟确认对话框
window.confirm = vi.fn().mockReturnValue(true);
await user.click(deleteButtons[0]);
// 验证确认对话框被调用
expect(window.confirm).toHaveBeenCalledWith('确定要删除这条路线吗?');
}
});
it('应该处理车型筛选', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 验证车型筛选器存在 - 使用data-testid更精确
const vehicleTypeFilter = screen.getByTestId('route-vehicle-type-filter');
expect(vehicleTypeFilter).toBeInTheDocument();
// 点击Select来展开选项
await user.click(vehicleTypeFilter);
// 验证筛选选项存在
expect(screen.getByText('大巴')).toBeInTheDocument();
expect(screen.getByText('中巴')).toBeInTheDocument();
expect(screen.getByText('小车')).toBeInTheDocument();
});
it('应该处理API错误场景', async () => {
// 模拟API错误
(routeClient.$get as any).mockResolvedValueOnce({
status: 500,
ok: false,
json: async () => ({ error: 'Internal server error' })
});
render(
);
// 验证页面仍然渲染基本结构
expect(screen.getByText('路线管理')).toBeInTheDocument();
expect(screen.getByText('新建路线')).toBeInTheDocument();
// 验证错误处理(组件应该优雅处理错误)
await waitFor(() => {
expect(screen.queryByText('北京到上海路线')).not.toBeInTheDocument();
});
});
it('应该显示筛选标签', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 输入搜索关键词
const searchInput = screen.getByPlaceholderText('搜索路线名称、地点或车型...');
await user.type(searchInput, '北京');
// 等待防抖搜索生效
await waitFor(() => {
expect(screen.getByText('搜索: 北京')).toBeInTheDocument();
});
// 验证筛选标签显示,但不直接点击组合框避免事件错误
expect(screen.getByText('搜索: 北京')).toBeInTheDocument();
});
it('应该清除筛选标签', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 输入搜索关键词
const searchInput = screen.getByPlaceholderText('搜索路线名称、地点或车型...');
await user.type(searchInput, '北京');
// 等待筛选标签显示
await waitFor(() => {
expect(screen.getByText('搜索: 北京')).toBeInTheDocument();
});
// 清除搜索筛选
const clearSearchButton = screen.getByText('×', { selector: 'button' });
await user.click(clearSearchButton);
// 验证搜索筛选被清除
await waitFor(() => {
expect(screen.queryByText('搜索: 北京')).not.toBeInTheDocument();
});
});
it('应该显示关联活动信息', async () => {
render(
);
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('北京到上海路线')).toBeInTheDocument();
});
// 验证关联活动信息显示
expect(screen.getByText('北京去程活动')).toBeInTheDocument();
expect(screen.getByText('上海返程活动')).toBeInTheDocument();
});
});