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 { LocationsPage } from '@/client/admin/pages/Locations';
import { AdminTestWrapper } from '~/utils/client/test-render';
// Import mocked modules
import { locationClient } from '@/client/api';
// Mock next-themes 组件
vi.mock('next-themes', () => ({
ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
useTheme: () => ({
theme: 'light',
setTheme: vi.fn(),
}),
}));
// Mock API 客户端
vi.mock('@/client/api', () => ({
locationClient: {
$get: vi.fn().mockResolvedValue({
status: 200,
ok: true,
json: async () => ({
data: [
{
id: 1,
name: '北京天安门',
address: '北京市东城区天安门广场',
province: {
id: 1,
name: '北京市',
code: '110000'
},
city: {
id: 2,
name: '北京市',
code: '110100'
},
district: {
id: 3,
name: '东城区',
code: '110101'
},
latitude: 39.9042,
longitude: 116.4074,
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z'
},
{
id: 2,
name: '上海外滩',
address: '上海市黄浦区中山东一路',
province: {
id: 4,
name: '上海市',
code: '310000'
},
city: {
id: 5,
name: '上海市',
code: '310100'
},
district: {
id: 6,
name: '黄浦区',
code: '310101'
},
latitude: 31.2304,
longitude: 121.4737,
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z'
}
],
pagination: {
total: 2,
current: 1,
pageSize: 20
}
})
}),
$post: vi.fn().mockResolvedValue({
status: 201,
ok: true,
json: async () => ({
id: 3,
name: '新建地点',
address: '新建地址',
province: null,
city: null,
district: null,
latitude: null,
longitude: null,
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z'
})
}),
':id': {
$put: vi.fn().mockResolvedValue({
status: 200,
ok: true,
json: async () => ({
id: 1,
name: '更新后的地点',
address: '更新后的地址',
province: {
id: 1,
name: '北京市',
code: '110000'
},
city: {
id: 2,
name: '北京市',
code: '110100'
},
district: {
id: 3,
name: '东城区',
code: '110101'
},
latitude: 39.9042,
longitude: 116.4074,
isDisabled: 0
})
}),
$delete: vi.fn().mockResolvedValue({
status: 204,
ok: true
})
}
},
areaClient: {
$get: vi.fn().mockResolvedValue({
status: 200,
ok: true,
json: async () => ({
data: [
{
id: 1,
name: '华北地区',
code: 'north_china',
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z'
},
{
id: 2,
name: '华东地区',
code: 'east_china',
isDisabled: 0,
createdAt: '2024-01-01T00:00:00.000Z'
}
],
pagination: {
total: 2,
current: 1,
pageSize: 100
}
})
})
}
}));
describe('LocationsPage 集成测试', () => {
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 areaFilter = screen.getByPlaceholderText('选择区域');
expect(areaFilter).toBeInTheDocument();
// 点击Select来展开选项
await user.click(areaFilter);
// 验证筛选选项存在 - 检查选项数量
const options = screen.getAllByText(/华北地区|华东地区/);
expect(options.length).toBeGreaterThanOrEqual(2);
});
it('应该显示状态筛选功能', async () => {
const user = userEvent.setup();
render(
);
// 等待数据加载
await waitFor(() => {
expect(screen.getByText('地点列表')).toBeInTheDocument();
});
// 验证状态筛选器存在
const statusFilter = screen.getByPlaceholderText('状态筛选');
expect(statusFilter).toBeInTheDocument();
// 点击Select来展开选项
await user.click(statusFilter);
// 验证筛选选项存在
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('显示第 1 到 2 条,共 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();
});
});
it('应该显示地点数据在表格中', async () => {
render(
);
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('北京天安门')).toBeInTheDocument();
expect(screen.getByText('上海外滩')).toBeInTheDocument();
expect(screen.getByText('北京市东城区天安门广场')).toBeInTheDocument();
expect(screen.getByText('上海市黄浦区中山东一路')).toBeInTheDocument();
expect(screen.getAllByText('启用').length).toBeGreaterThan(0);
});
});
it('应该显示所属区域信息', async () => {
render(
);
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('北京天安门')).toBeInTheDocument();
});
// 验证区域信息显示
expect(screen.getByText('北京市')).toBeInTheDocument();
expect(screen.getByText('东城区')).toBeInTheDocument();
});
it('应该显示坐标信息', async () => {
render(
);
// 等待数据加载完成
await waitFor(() => {
expect(screen.getByText('北京天安门')).toBeInTheDocument();
});
// 验证坐标显示
expect(screen.getByText('39.904200, 116.407400')).toBeInTheDocument();
});
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.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 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();
});
// 验证区域筛选器存在
const areaFilter = screen.getByPlaceholderText('选择区域');
expect(areaFilter).toBeInTheDocument();
// 点击Select来展开选项
await user.click(areaFilter);
// 验证筛选选项存在 - 检查选项数量
const options = screen.getAllByText(/华北地区|华东地区/);
expect(options.length).toBeGreaterThanOrEqual(2);
});
it('应该处理状态筛选', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.getByText('北京天安门')).toBeInTheDocument();
});
// 验证状态筛选器存在
const statusFilter = screen.getByPlaceholderText('状态筛选');
expect(statusFilter).toBeInTheDocument();
// 点击Select来展开选项
await user.click(statusFilter);
// 验证筛选选项存在
expect(screen.getByText('全部状态')).toBeInTheDocument();
expect(screen.getByText('启用')).toBeInTheDocument();
expect(screen.getByText('禁用')).toBeInTheDocument();
});
it('应该处理API错误场景', async () => {
// 模拟API错误
(locationClient.$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 () => {
// 模拟空数据
(locationClient.$get as any).mockResolvedValueOnce({
status: 200,
ok: true,
json: async () => ({
data: [],
pagination: {
total: 0,
current: 1,
pageSize: 20
}
})
});
render(
);
// 验证空状态显示
await waitFor(() => {
expect(screen.getByText('暂无地点数据')).toBeInTheDocument();
});
});
it('应该显示加载状态', async () => {
// 模拟加载延迟
(locationClient.$get as any).mockImplementationOnce(() =>
new Promise(resolve => setTimeout(() => resolve({
status: 200,
ok: true,
json: async () => ({
data: [],
pagination: { total: 0, current: 1, pageSize: 20 }
})
}), 100))
);
render(
);
// 验证加载状态显示
expect(screen.getByText('加载中...')).toBeInTheDocument();
});
});