// REQ-USR-003: UserListPage 页面集成(状态机贯通 + 导航 + 错误重试,BR3/BR7/BR8/BR9/BR12/BR13/BR15) import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Routes, Route, useLocation } from 'react-router-dom'; import { renderShell } from './renderShell'; // 桩 message const messageSpy = { success: vi.fn(), error: vi.fn(), warning: vi.fn() }; vi.mock('antd', async () => { const actual = await vi.importActual('antd'); return { ...actual, App: Object.assign(actual.App, { useApp: () => ({ message: messageSpy }) }), }; }); vi.mock('../../src/api/usrApi', () => ({ listUsers: vi.fn() })); import { listUsers } from '../../src/api/usrApi'; import UserListPage from '../../src/pages/usr/UserList'; import { ApiError } from '../../src/api/request'; import type { UserVO } from '../../src/api/types'; const mockedList = listUsers as unknown as ReturnType; function makeUser(id: number, name = `user${id}`): UserVO { return { id, sUserName: name, employeeName: null, sUserNo: null, departmentName: null, sUserType: '普通用户', sLanguage: '中文', iIsVoid: 0, tLastLoginDate: null, sCreator: 'admin', tCreateDate: '2024-01-01T00:00:00', }; } function page(records: UserVO[], total: number, pageNum = 1, pageSize = 10) { return { records, total, pageNum, pageSize }; } function LocationProbe() { const loc = useLocation(); return
{loc.pathname}
; } function renderPage() { return renderShell( <> } /> new} /> detail} /> , { initialEntries: ['/usr/users'], preloadedAuth: { token: 't', user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' }, }, }, ); } function lastQuery() { const calls = mockedList.mock.calls; return calls[calls.length - 1][0]; } describe('UserListPage 集成', () => { beforeEach(() => { vi.clearAllMocks(); }); it('initial load renders rows from listUsers (default query) (BR2)', async () => { mockedList.mockResolvedValue(page([makeUser(1), makeUser(2)], 2)); renderPage(); await waitFor(() => expect(mockedList).toHaveBeenCalled()); expect(mockedList.mock.calls[0][0]).toMatchObject({ queryField: '用户名', matchType: '包含', pageNum: 1, pageSize: 10, }); expect(await screen.findByText('user1')).toBeInTheDocument(); expect(await screen.findByText('user2')).toBeInTheDocument(); }); it('search with value submits queryValue and shows results (BR7/BR3)', async () => { const user = userEvent.setup(); mockedList.mockResolvedValue(page([makeUser(9, '李雷')], 1)); renderPage(); await waitFor(() => expect(mockedList).toHaveBeenCalled()); const input = screen.getByTestId('filter-query-value').querySelector('input')!; await user.type(input, '李'); await user.click(screen.getByTestId('btn-search')); await waitFor(() => { const q = lastQuery(); expect(q.queryValue).toBe('李'); expect(q.pageNum).toBe(1); }); expect(await screen.findByText('李雷')).toBeInTheDocument(); }); it('empty response shows 暂无匹配的用户 (BR14)', async () => { mockedList.mockResolvedValue(page([], 0)); renderPage(); expect(await screen.findByText('暂无匹配的用户')).toBeInTheDocument(); expect(messageSpy.error).not.toHaveBeenCalled(); }); it('error response shows 点击重试; retry calls refresh', async () => { const user = userEvent.setup(); mockedList.mockRejectedValueOnce(new ApiError(-1, '网络异常')); renderPage(); expect(await screen.findByText('加载失败,点击重试')).toBeInTheDocument(); mockedList.mockResolvedValueOnce(page([makeUser(3)], 1)); await user.click(screen.getByTestId('userlist-error').querySelector('button')!); expect(await screen.findByText('user3')).toBeInTheDocument(); }); it('新增 navigates to /usr/users/new (BR13)', async () => { const user = userEvent.setup(); mockedList.mockResolvedValue(page([makeUser(1)], 1)); renderPage(); await waitFor(() => expect(mockedList).toHaveBeenCalled()); await user.click(screen.getByTestId('btn-add')); expect(screen.getByTestId('loc').textContent).toBe('/usr/users/new'); expect(screen.getByTestId('new-sentinel')).toBeInTheDocument(); }); it('double click row navigates to /usr/users/:id (BR12)', async () => { const user = userEvent.setup(); mockedList.mockResolvedValue(page([makeUser(42)], 1)); renderPage(); await screen.findByText('user42'); await user.dblClick(screen.getByText('user42')); expect(screen.getByTestId('loc').textContent).toBe('/usr/users/42'); expect(screen.getByTestId('detail-sentinel')).toBeInTheDocument(); }); it('refresh keeps current page (BR8)', async () => { const user = userEvent.setup(); // 初次加载回显第 1 页 mockedList.mockResolvedValueOnce(page([makeUser(1)], 30, 1, 10)); renderPage(); await waitFor(() => expect(mockedList).toHaveBeenCalled()); await screen.findByText('user1'); // 翻到第 2 页(点下一页),响应回显 pageNum=2 mockedList.mockResolvedValueOnce(page([makeUser(1)], 30, 2, 10)); await user.click(screen.getByTitle('下一页')); await waitFor(() => expect(lastQuery().pageNum).toBe(2)); mockedList.mockClear(); mockedList.mockResolvedValue(page([makeUser(1)], 30, 2, 10)); await user.click(screen.getByTestId('btn-refresh')); await waitFor(() => expect(mockedList).toHaveBeenCalled()); // 刷新保持当前页(2),不回第 1 页 expect(lastQuery().pageNum).toBe(2); }); it('response pageNum echo syncs pagination (BR15)', async () => { mockedList.mockResolvedValue(page([makeUser(1)], 50, 5, 10)); renderPage(); // 后端回显 pageNum=5,分页当前页应跟随响应 await screen.findByText('user1'); await waitFor(() => { const pager = screen.getByTestId('user-table'); expect(within(pager).getByTitle('5')).toHaveClass('ant-pagination-item-active'); }); }); });