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(); }); });