disabled-person-selector.integration.test.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import { describe, it, expect, vi, beforeEach } from 'vitest';
  2. import { render, screen, fireEvent, waitFor } from '@testing-library/react';
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  4. import DisabledPersonSelector from '../../src/components/DisabledPersonSelector';
  5. import { disabilityClientManager } from '../../src/api/disabilityClient';
  6. import type { DisabledPersonData } from '../../src/api/types';
  7. // 完整的mock响应对象
  8. const createMockResponse = (status: number, data?: any) => ({
  9. status,
  10. ok: status >= 200 && status < 300,
  11. body: null,
  12. bodyUsed: false,
  13. statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
  14. headers: new Headers(),
  15. url: '',
  16. redirected: false,
  17. type: 'basic' as ResponseType,
  18. json: async () => data || {},
  19. text: async () => '',
  20. blob: async () => new Blob(),
  21. arrayBuffer: async () => new ArrayBuffer(0),
  22. formData: async () => new FormData(),
  23. clone: function() { return this; }
  24. });
  25. // Mock API client
  26. vi.mock('../../src/api/disabilityClient', () => {
  27. const mockDisabilityClient = {
  28. searchDisabledPersons: {
  29. $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
  30. data: [
  31. {
  32. id: 1,
  33. name: '张三',
  34. gender: '男',
  35. idCard: '110101199001011234',
  36. disabilityId: 'CJZ20240001',
  37. disabilityType: '视力残疾',
  38. disabilityLevel: '一级',
  39. idAddress: '北京市东城区',
  40. phone: '13800138000',
  41. province: '北京市',
  42. city: '北京市',
  43. district: '东城区',
  44. isInBlackList: 0,
  45. createTime: '2024-01-01T00:00:00Z',
  46. updateTime: '2024-01-01T00:00:00Z'
  47. },
  48. {
  49. id: 2,
  50. name: '李四',
  51. gender: '女',
  52. idCard: '110101199001011235',
  53. disabilityId: 'CJZ20240002',
  54. disabilityType: '听力残疾',
  55. disabilityLevel: '二级',
  56. idAddress: '上海市黄浦区',
  57. phone: '13800138001',
  58. province: '上海市',
  59. city: '上海市',
  60. district: '黄浦区',
  61. isInBlackList: 1, // 黑名单人员
  62. createTime: '2024-01-01T00:00:00Z',
  63. updateTime: '2024-01-01T00:00:00Z'
  64. }
  65. ],
  66. total: 2
  67. })))
  68. },
  69. getAllDisabledPersons: {
  70. $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
  71. data: [
  72. {
  73. id: 3,
  74. name: '王五',
  75. gender: '男',
  76. idCard: '110101199001011236',
  77. disabilityId: 'CJZ20240003',
  78. disabilityType: '肢体残疾',
  79. disabilityLevel: '三级',
  80. idAddress: '广州市天河区',
  81. phone: '13800138002',
  82. province: '广东省',
  83. city: '广州市',
  84. district: '天河区',
  85. isInBlackList: 0,
  86. createTime: '2024-01-01T00:00:00Z',
  87. updateTime: '2024-01-01T00:00:00Z'
  88. }
  89. ],
  90. total: 1
  91. })))
  92. }
  93. };
  94. const mockClientManager = {
  95. get: vi.fn(() => mockDisabilityClient),
  96. init: vi.fn(() => mockDisabilityClient),
  97. reset: vi.fn(),
  98. getInstance: vi.fn(() => mockClientManager)
  99. };
  100. return {
  101. disabilityClientManager: mockClientManager,
  102. disabilityClient: mockDisabilityClient
  103. };
  104. });
  105. // Mock AreaSelect组件
  106. vi.mock('@d8d/area-management-ui', () => ({
  107. AreaSelect: ({ value, onChange, placeholder }: any) => (
  108. <div data-testid="area-select">
  109. <select
  110. value={value?.provinceId || ''}
  111. onChange={(e) => onChange({ provinceId: e.target.value ? Number(e.target.value) : undefined })}
  112. data-testid="area-select-input"
  113. >
  114. <option value="">{placeholder || '选择地区'}</option>
  115. <option value="1">北京市</option>
  116. <option value="2">上海市</option>
  117. </select>
  118. </div>
  119. )
  120. }));
  121. // Mock shared-ui-components
  122. vi.mock('@d8d/shared-ui-components/components/ui/dialog', () => ({
  123. Dialog: ({ children, open, onOpenChange }: any) => open ? (
  124. <div data-testid="dialog">{children}</div>
  125. ) : null,
  126. DialogContent: ({ children, className }: any) => (
  127. <div data-testid="dialog-content" className={className}>{children}</div>
  128. ),
  129. DialogHeader: ({ children }: any) => (
  130. <div data-testid="dialog-header">{children}</div>
  131. ),
  132. DialogTitle: ({ children }: any) => (
  133. <h2 data-testid="dialog-title">{children}</h2>
  134. ),
  135. DialogFooter: ({ children, className }: any) => (
  136. <div data-testid="dialog-footer" className={className}>{children}</div>
  137. )
  138. }));
  139. vi.mock('@d8d/shared-ui-components/components/ui/button', () => ({
  140. Button: ({ children, onClick, disabled, variant, ...props }: any) => (
  141. <button
  142. onClick={onClick}
  143. disabled={disabled}
  144. data-testid={props['data-testid'] || 'button'}
  145. data-variant={variant}
  146. {...props}
  147. >
  148. {children}
  149. </button>
  150. )
  151. }));
  152. vi.mock('@d8d/shared-ui-components/components/ui/input', () => ({
  153. Input: ({ value, onChange, placeholder, ...props }: any) => (
  154. <input
  155. type="text"
  156. value={value}
  157. onChange={onChange}
  158. placeholder={placeholder}
  159. data-testid={props['data-testid'] || 'input'}
  160. {...props}
  161. />
  162. )
  163. }));
  164. vi.mock('@d8d/shared-ui-components/components/ui/select', () => ({
  165. Select: ({ children }: any) => (
  166. <div data-testid="select">
  167. {children}
  168. </div>
  169. ),
  170. SelectTrigger: ({ children }: any) => (
  171. <div data-testid="select-trigger">{children}</div>
  172. ),
  173. SelectValue: ({ placeholder }: any) => (
  174. <span data-testid="select-value">{placeholder}</span>
  175. ),
  176. SelectContent: ({ children }: any) => (
  177. <div data-testid="select-content">{children}</div>
  178. ),
  179. SelectItem: ({ children, value }: any) => (
  180. <option value={value} data-testid={`select-item-${value}`}>{children}</option>
  181. )
  182. }));
  183. vi.mock('@d8d/shared-ui-components/components/ui/label', () => ({
  184. Label: ({ children, htmlFor }: any) => (
  185. <label htmlFor={htmlFor} data-testid="label">{children}</label>
  186. )
  187. }));
  188. vi.mock('@d8d/shared-ui-components/components/ui/table', () => ({
  189. Table: ({ children, ...props }: any) => (
  190. <table data-testid={props['data-testid'] || 'table'}>{children}</table>
  191. ),
  192. TableHeader: ({ children }: any) => <thead data-testid="table-header">{children}</thead>,
  193. TableBody: ({ children }: any) => <tbody data-testid="table-body">{children}</tbody>,
  194. TableRow: ({ children, onClick, className, ...props }: any) => (
  195. <tr onClick={onClick} className={className} data-testid={props['data-testid'] || 'table-row'}>{children}</tr>
  196. ),
  197. TableHead: ({ children, className }: any) => <th className={className}>{children}</th>,
  198. TableCell: ({ children }: any) => <td>{children}</td>,
  199. }));
  200. vi.mock('@d8d/shared-ui-components/components/admin/DataTablePagination', () => ({
  201. DataTablePagination: ({ currentPage, pageSize, totalCount, onPageChange, ...props }: any) => (
  202. <div data-testid={props['data-testid'] || 'pagination'}>
  203. <button onClick={() => onPageChange(currentPage - 1, pageSize)} data-testid="prev-page">上一页</button>
  204. <span>第{currentPage}页,共{Math.ceil(totalCount / pageSize)}页</span>
  205. <button onClick={() => onPageChange(currentPage + 1, pageSize)} data-testid="next-page">下一页</button>
  206. </div>
  207. )
  208. }));
  209. vi.mock('@d8d/shared-ui-components/components/ui/checkbox', () => ({
  210. Checkbox: ({ checked, onCheckedChange, disabled, ...props }: any) => (
  211. <input
  212. type="checkbox"
  213. checked={checked}
  214. onChange={(e) => onCheckedChange && onCheckedChange(e.target.checked)}
  215. disabled={disabled}
  216. data-testid={props['data-testid'] || 'checkbox'}
  217. {...props}
  218. />
  219. )
  220. }));
  221. vi.mock('@d8d/shared-ui-components/components/ui/alert', () => ({
  222. Alert: ({ children, variant }: any) => (
  223. <div data-testid="alert" data-variant={variant}>{children}</div>
  224. ),
  225. AlertDescription: ({ children }: any) => (
  226. <div data-testid="alert-description">{children}</div>
  227. )
  228. }));
  229. vi.mock('lucide-react', () => ({
  230. AlertCircle: () => <div data-testid="alert-circle">⚠️</div>
  231. }));
  232. describe('DisabledPersonSelector', () => {
  233. let queryClient: QueryClient;
  234. let onOpenChange: ReturnType<typeof vi.fn>;
  235. let onSelect: ReturnType<typeof vi.fn>;
  236. beforeEach(() => {
  237. queryClient = new QueryClient({
  238. defaultOptions: {
  239. queries: {
  240. retry: false,
  241. },
  242. },
  243. });
  244. onOpenChange = vi.fn();
  245. onSelect = vi.fn();
  246. vi.clearAllMocks();
  247. });
  248. const renderComponent = (props = {}) => {
  249. return render(
  250. <QueryClientProvider client={queryClient}>
  251. <DisabledPersonSelector
  252. open={true}
  253. onOpenChange={onOpenChange}
  254. onSelect={onSelect}
  255. {...props}
  256. />
  257. </QueryClientProvider>
  258. );
  259. };
  260. it('应该渲染对话框和搜索区域', () => {
  261. renderComponent();
  262. expect(screen.getByTestId('dialog')).toBeInTheDocument();
  263. expect(screen.getByTestId('dialog-title')).toHaveTextContent('选择残疾人');
  264. // 检查搜索字段
  265. expect(screen.getByTestId('search-name-input')).toBeInTheDocument();
  266. expect(screen.getByTestId('area-select')).toBeInTheDocument();
  267. expect(screen.getByTestId('search-button')).toBeInTheDocument();
  268. expect(screen.getByTestId('reset-button')).toBeInTheDocument();
  269. });
  270. it('应该显示残疾人列表', async () => {
  271. renderComponent();
  272. // 等待数据加载
  273. await waitFor(() => {
  274. expect(screen.getByTestId('data-table')).toBeInTheDocument();
  275. });
  276. // 检查表格数据
  277. expect(screen.getByText('张三')).toBeInTheDocument();
  278. expect(screen.getByText('男')).toBeInTheDocument();
  279. expect(screen.getByText('110101199001011234')).toBeInTheDocument();
  280. });
  281. it('应该处理搜索功能', async () => {
  282. renderComponent();
  283. // 输入搜索关键词
  284. const searchInput = screen.getByTestId('search-name-input');
  285. fireEvent.change(searchInput, { target: { value: '张三' } });
  286. // 点击搜索按钮
  287. const searchButton = screen.getByTestId('search-button');
  288. fireEvent.click(searchButton);
  289. // 验证搜索API被调用
  290. await waitFor(() => {
  291. expect(disabilityClientManager.get().searchDisabledPersons.$get).toHaveBeenCalledWith({
  292. query: {
  293. keyword: '张三',
  294. skip: 0,
  295. take: 10
  296. }
  297. });
  298. });
  299. });
  300. it('应该处理重置搜索', () => {
  301. renderComponent();
  302. // 输入搜索关键词
  303. const searchInput = screen.getByTestId('search-name-input');
  304. fireEvent.change(searchInput, { target: { value: '张三' } });
  305. // 点击重置按钮
  306. const resetButton = screen.getByTestId('reset-button');
  307. fireEvent.click(resetButton);
  308. // 验证搜索输入被清空
  309. expect(searchInput).toHaveValue('');
  310. });
  311. it('应该处理单选模式', async () => {
  312. renderComponent({ mode: 'single' });
  313. await waitFor(() => {
  314. expect(screen.getByTestId('data-table')).toBeInTheDocument();
  315. });
  316. // 点击表格行选择人员
  317. const firstRow = screen.getByTestId('table-row-0');
  318. fireEvent.click(firstRow);
  319. // 验证选择回调被调用
  320. expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({
  321. id: 1,
  322. name: '张三'
  323. }));
  324. expect(onOpenChange).toHaveBeenCalledWith(false);
  325. });
  326. it('应该处理多选模式', async () => {
  327. renderComponent({ mode: 'multiple' });
  328. await waitFor(() => {
  329. expect(screen.getByTestId('data-table')).toBeInTheDocument();
  330. });
  331. // 应该显示多选相关的UI
  332. expect(screen.getAllByTestId('checkbox')).toHaveLength(3); // 全选复选框 + 每行复选框
  333. // 点击确认批量选择按钮(初始时禁用)
  334. const confirmButton = screen.getByTestId('confirm-batch-button');
  335. expect(confirmButton).toBeDisabled();
  336. });
  337. it('应该处理黑名单人员确认', async () => {
  338. renderComponent({ mode: 'single' });
  339. await waitFor(() => {
  340. expect(screen.getByTestId('data-table')).toBeInTheDocument();
  341. });
  342. // 点击黑名单人员(李四)
  343. const blacklistRow = screen.getByTestId('table-row-1');
  344. fireEvent.click(blacklistRow);
  345. // 应该显示黑名单确认对话框
  346. await waitFor(() => {
  347. expect(screen.getByTestId('alert')).toBeInTheDocument();
  348. expect(screen.getByTestId('alert-description')).toHaveTextContent('您选择的人员在黑名单中,是否确认选择?');
  349. });
  350. // 点击确认选择
  351. const confirmButton = screen.getByTestId('confirm-blacklist-button');
  352. fireEvent.click(confirmButton);
  353. // 验证选择回调被调用
  354. expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({
  355. id: 2,
  356. name: '李四',
  357. isInBlackList: 1
  358. }));
  359. expect(onOpenChange).toHaveBeenCalledWith(false);
  360. });
  361. it('应该处理禁用的人员', async () => {
  362. renderComponent({
  363. mode: 'single',
  364. disabledIds: [1] // 禁用张三
  365. });
  366. await waitFor(() => {
  367. expect(screen.getByTestId('data-table')).toBeInTheDocument();
  368. });
  369. // 点击禁用的人员应该没有反应
  370. const disabledRow = screen.getByTestId('table-row-0');
  371. fireEvent.click(disabledRow);
  372. expect(onSelect).not.toHaveBeenCalled();
  373. });
  374. it('应该处理分页', async () => {
  375. renderComponent();
  376. await waitFor(() => {
  377. expect(screen.getByTestId('pagination')).toBeInTheDocument();
  378. });
  379. // 点击下一页
  380. const nextPageButton = screen.getByTestId('next-page');
  381. fireEvent.click(nextPageButton);
  382. // 验证API被调用(第2页)
  383. await waitFor(() => {
  384. expect(disabilityClientManager.get().getAllDisabledPersons.$get).toHaveBeenCalledWith({
  385. query: {
  386. skip: 10,
  387. take: 10
  388. }
  389. });
  390. });
  391. });
  392. it('应该处理对话框关闭', () => {
  393. renderComponent();
  394. // 点击取消按钮
  395. const cancelButton = screen.getByTestId('cancel-button');
  396. fireEvent.click(cancelButton);
  397. expect(onOpenChange).toHaveBeenCalledWith(false);
  398. });
  399. });