UserDetailPage.test.tsx 8.74 KB
// REQ-USR-001 / REQ-USR-002: UserDetailPage 页面集成 + 路由接线
// create/edit 贯通 + 提交回流 + 错误就近 + 取数失败(BR3/BR12/BR16/BR17/D4/D5)
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';

const messageSpy = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: 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', () => ({
  createUser: vi.fn(),
  updateUser: vi.fn(),
  getUserDetail: vi.fn(),
  listEmployees: vi.fn(),
  listPermissions: vi.fn(),
}));

import {
  createUser,
  updateUser,
  getUserDetail,
  listEmployees,
  listPermissions,
} from '../../src/api/usrApi';
import UserDetailPage from '../../src/pages/usr/UserDetail';
import { ApiError } from '../../src/api/request';
import { ERR_USERNAME_EXISTS } from '../../src/pages/usr/UserDetail/constants';
import type { UserVO, EmployeeOption, PermissionItem } from '../../src/api/types';

const mockedCreate = createUser as unknown as ReturnType<typeof vi.fn>;
const mockedUpdate = updateUser as unknown as ReturnType<typeof vi.fn>;
const mockedDetail = getUserDetail as unknown as ReturnType<typeof vi.fn>;
const mockedEmployees = listEmployees as unknown as ReturnType<typeof vi.fn>;
const mockedPermissions = listPermissions as unknown as ReturnType<typeof vi.fn>;

const EMPLOYEES: EmployeeOption[] = [{ value: 3, label: '张三', sEmployeeNo: 'zs' }];
const PERMISSIONS: PermissionItem[] = [
  { id: 1, name: '默认显示', category: '基础' },
  { id: 2, name: '高级查看', category: '基础' },
];

function makeVo(over: Partial<UserVO> = {}): UserVO {
  return {
    id: 7,
    sUserName: 'zhangsan',
    employeeName: '张三',
    sUserNo: 'zs',
    departmentName: null,
    sUserType: '超级管理员',
    sLanguage: '英文',
    iIsVoid: 0,
    tLastLoginDate: null,
    sCreator: 'admin',
    tCreateDate: '2026-01-01T00:00:00',
    ...over,
  };
}

function LocationProbe() {
  const loc = useLocation();
  return <div data-testid="loc">{loc.pathname}</div>;
}

function renderPage(entry: string) {
  return renderShell(
    <>
      <LocationProbe />
      <Routes>
        <Route path="/usr/users" element={<div data-testid="list-sentinel">list</div>} />
        <Route path="/usr/users/new" element={<UserDetailPage />} />
        <Route path="/usr/users/:id" element={<UserDetailPage />} />
      </Routes>
    </>,
    {
      initialEntries: [entry],
      preloadedAuth: {
        token: 't',
        user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' },
      },
    },
  );
}

async function fillValidCreateForm(user: ReturnType<typeof userEvent.setup>) {
  await user.type(screen.getByTestId('field-username'), 'zhangsan');
  await user.type(screen.getByTestId('field-userno'), 'zs');
  // 语言必填
  await user.click(screen.getByTestId('select-language').querySelector('.ant-select-selector')!);
  await user.click(await screen.findByText('中文'));
}

describe('UserDetailPage 集成', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockedEmployees.mockResolvedValue(EMPLOYEES);
    mockedPermissions.mockResolvedValue(PERMISSIONS);
  });

  it('create mode renders empty form with defaults', async () => {
    renderPage('/usr/users/new');
    await waitFor(() => expect(mockedEmployees).toHaveBeenCalled());
    expect(await screen.findByText('保存后自动生成')).toBeInTheDocument();
    expect(within(screen.getByTestId('select-usertype')).getByText('普通用户')).toBeInTheDocument();
  });

  it('create submit success navigates to /usr/users with success', async () => {
    const user = userEvent.setup();
    mockedCreate.mockResolvedValue({ id: 9 });
    renderPage('/usr/users/new');
    await waitFor(() => expect(mockedEmployees).toHaveBeenCalled());
    await fillValidCreateForm(user);
    await user.click(screen.getByTestId('perm-check-1'));
    await user.click(screen.getByTestId('btn-save'));
    await waitFor(() => expect(mockedCreate).toHaveBeenCalled());
    const body = mockedCreate.mock.calls[0][0];
    expect(body.sUserName).toBe('zhangsan');
    expect(body.permissionIds).toContain(1);
    expect(messageSpy.success).toHaveBeenCalledWith('用户创建成功');
    await waitFor(() => expect(screen.getByTestId('loc').textContent).toBe('/usr/users'));
  });

  it('create username format invalid blocks submit', async () => {
    const user = userEvent.setup();
    renderPage('/usr/users/new');
    await waitFor(() => expect(mockedEmployees).toHaveBeenCalled());
    await user.type(screen.getByTestId('field-username'), 'ab');
    await user.click(screen.getByTestId('btn-save'));
    expect(await screen.findByText('用户名须为 3-20 位字母数字下划线')).toBeInTheDocument();
    expect(mockedCreate).not.toHaveBeenCalled();
  });

  it('create 40901 highlights username field', async () => {
    const user = userEvent.setup();
    mockedCreate.mockRejectedValue(new ApiError(ERR_USERNAME_EXISTS, 'dup'));
    renderPage('/usr/users/new');
    await waitFor(() => expect(mockedEmployees).toHaveBeenCalled());
    await fillValidCreateForm(user);
    await user.click(screen.getByTestId('btn-save'));
    await waitFor(() => expect(mockedCreate).toHaveBeenCalled());
    expect(await screen.findByText('用户名已存在,请更换')).toBeInTheDocument();
  });

  it('edit mode prefills from getUserDetail and username disabled', async () => {
    mockedDetail.mockResolvedValue(makeVo());
    renderPage('/usr/users/7');
    await waitFor(() => expect(mockedDetail).toHaveBeenCalled());
    await waitFor(() =>
      expect((screen.getByTestId('field-username') as HTMLInputElement).value).toBe('zhangsan'),
    );
    expect(screen.getByTestId('field-username')).toBeDisabled();
  });

  it('edit submit success navigates to /usr/users with 保存成功', async () => {
    const user = userEvent.setup();
    mockedDetail.mockResolvedValue(makeVo());
    mockedUpdate.mockResolvedValue({ id: 7 });
    renderPage('/usr/users/7');
    await waitFor(() => expect(mockedDetail).toHaveBeenCalled());
    await waitFor(() =>
      expect((screen.getByTestId('field-username') as HTMLInputElement).value).toBe('zhangsan'),
    );
    await user.click(screen.getByTestId('btn-save'));
    await waitFor(() => expect(mockedUpdate).toHaveBeenCalled());
    expect(mockedUpdate.mock.calls[0][0]).toBe(7);
    expect(messageSpy.success).toHaveBeenCalledWith('保存成功');
    await waitFor(() => expect(screen.getByTestId('loc').textContent).toBe('/usr/users'));
  });

  it('cancel with dirty form confirms then navigates', async () => {
    const user = userEvent.setup();
    renderPage('/usr/users/new');
    await waitFor(() => expect(mockedEmployees).toHaveBeenCalled());
    await user.type(screen.getByTestId('field-username'), 'dirtyuser');
    await user.click(screen.getByTestId('btn-cancel'));
    // AntD Modal.confirm 弹确认
    expect((await screen.findAllByText('放弃未保存的修改?')).length).toBeGreaterThan(0);
    await user.click(screen.getByRole('button', { name: /确\s*定|OK/ }));
    await waitFor(() => expect(screen.getByTestId('loc').textContent).toBe('/usr/users'));
  });

  it('新增 navigates to /usr/users/new', async () => {
    const user = userEvent.setup();
    mockedDetail.mockResolvedValue(makeVo());
    renderPage('/usr/users/7');
    await waitFor(() => expect(mockedDetail).toHaveBeenCalled());
    await user.click(screen.getByTestId('btn-new'));
    await waitFor(() => expect(screen.getByTestId('loc').textContent).toBe('/usr/users/new'));
  });

  it('loadError shows retry; retry calls reload', async () => {
    mockedPermissions.mockRejectedValueOnce(new ApiError(-1, 'net'));
    renderPage('/usr/users/new');
    expect(await screen.findByTestId('userdetail-loaderror')).toBeInTheDocument();
    mockedPermissions.mockResolvedValue(PERMISSIONS);
    const user = userEvent.setup();
    await user.click(within(screen.getByTestId('userdetail-loaderror')).getByText('点击重试'));
    await waitFor(() => expect(screen.queryByTestId('userdetail-loaderror')).toBeNull());
  });

  it('edit 40401 offers 返回列表', async () => {
    mockedDetail.mockResolvedValue(null);
    renderPage('/usr/users/7');
    expect(await screen.findByText('该用户不存在或已被删除')).toBeInTheDocument();
    const user = userEvent.setup();
    await user.click(screen.getByText('返回列表'));
    await waitFor(() => expect(screen.getByTestId('loc').textContent).toBe('/usr/users'));
  });
});