UserDetailPage.test.tsx
8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// 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'));
});
});