UserListPage.test.tsx 6.45 KB
// 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<typeof import('antd')>('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<typeof vi.fn>;

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 <div data-testid="loc">{loc.pathname}</div>;
}

function renderPage() {
  return renderShell(
    <>
      <LocationProbe />
      <Routes>
        <Route path="/usr/users" element={<UserListPage />} />
        <Route path="/usr/users/new" element={<div data-testid="new-sentinel">new</div>} />
        <Route path="/usr/users/:id" element={<div data-testid="detail-sentinel">detail</div>} />
      </Routes>
    </>,
    {
      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');
    });
  });
});