// 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('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; const mockedUpdate = updateUser as unknown as ReturnType; const mockedDetail = getUserDetail as unknown as ReturnType; const mockedEmployees = listEmployees as unknown as ReturnType; const mockedPermissions = listPermissions as unknown as ReturnType; const EMPLOYEES: EmployeeOption[] = [{ value: 3, label: '张三', sEmployeeNo: 'zs' }]; const PERMISSIONS: PermissionItem[] = [ { id: 1, name: '默认显示', category: '基础' }, { id: 2, name: '高级查看', category: '基础' }, ]; function makeVo(over: Partial = {}): 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
{loc.pathname}
; } // FE-04: edit 单据预填来自 FE-03 经 navigate state 透传的列表行(presetUser)。 // 测试入口可携带 state.user 复刻该数据流(路由 :id 仅为主键,无 by-id 读端点, // 不能按主键查「用户号」列——详见 useUserDetail / docs/05 REQ-USR-002/003)。 function renderPage(entry: string, presetUser?: UserVO) { const initialEntry = presetUser ? { pathname: entry, state: { user: presetUser } } : entry; return renderShell( <> list} /> } /> } /> , { initialEntries: [initialEntry], preloadedAuth: { token: 't', user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' }, }, }, ); } async function fillValidCreateForm(user: ReturnType) { 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 navigate state (presetUser) and username disabled', async () => { // FE-04: edit 预填走 FE-03 经 navigate state 透传的列表行,不再按主键查列表端点 renderPage('/usr/users/7', makeVo()); await waitFor(() => expect((screen.getByTestId('field-username') as HTMLInputElement).value).toBe('zhangsan'), ); expect(mockedDetail).not.toHaveBeenCalled(); expect(screen.getByTestId('field-username')).toBeDisabled(); }); it('edit submit success navigates to /usr/users with 保存成功', async () => { const user = userEvent.setup(); mockedUpdate.mockResolvedValue({ id: 7 }); renderPage('/usr/users/7', makeVo()); 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(); // edit 经 navigate state 预填后,工具栏「新增」跳 /usr/users/new(BR14) renderPage('/usr/users/7', makeVo()); await waitFor(() => expect((screen.getByTestId('field-username') as HTMLInputElement).value).toBe('zhangsan'), ); 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 without navigate state shows loadError offering 点击重试 + 返回列表', async () => { // FE-04 B1 fix: edit 缺 presetUser(直接访问 URL / 刷新丢 state)→ loadError, // 整页给「点击重试」与「返回列表」两个入口(spec § 4 loadError)。 renderPage('/usr/users/7'); const loadError = await screen.findByTestId('userdetail-loaderror'); expect(within(loadError).getByText('点击重试')).toBeInTheDocument(); expect(within(loadError).getByText('返回列表')).toBeInTheDocument(); expect(mockedDetail).not.toHaveBeenCalled(); const user = userEvent.setup(); await user.click(within(loadError).getByText('返回列表')); await waitFor(() => expect(screen.getByTestId('loc').textContent).toBe('/usr/users')); }); });