merchant-management.integration.test.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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 { MerchantManagement } from '../../src/components/MerchantManagement';
  5. import { merchantClientManager } from '../../src/api/merchantClient';
  6. // 完整的mock响应对象
  7. const createMockResponse = (status: number, data?: any) => ({
  8. status,
  9. ok: status >= 200 && status < 300,
  10. body: null,
  11. bodyUsed: false,
  12. statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
  13. headers: new Headers(),
  14. url: '',
  15. redirected: false,
  16. type: 'basic' as ResponseType,
  17. json: async () => data || {},
  18. text: async () => '',
  19. blob: async () => new Blob(),
  20. arrayBuffer: async () => new ArrayBuffer(0),
  21. formData: async () => new FormData(),
  22. clone: function() { return this; }
  23. });
  24. // Mock API client
  25. vi.mock('../../src/api/merchantClient', () => {
  26. const mockMerchantClient = {
  27. index: {
  28. $get: vi.fn(() => Promise.resolve({ status: 200, body: null })),
  29. $post: vi.fn(() => Promise.resolve({ status: 201, body: null })),
  30. },
  31. ':id': {
  32. $put: vi.fn(() => Promise.resolve({ status: 200, body: null })),
  33. $delete: vi.fn(() => Promise.resolve({ status: 204, body: null })),
  34. },
  35. };
  36. const mockMerchantClientManager = {
  37. get: vi.fn(() => mockMerchantClient),
  38. };
  39. return {
  40. merchantClientManager: mockMerchantClientManager,
  41. merchantClient: mockMerchantClient,
  42. };
  43. });
  44. // Mock toast
  45. vi.mock('sonner', () => ({
  46. toast: {
  47. success: vi.fn(() => {}),
  48. error: vi.fn(() => {}),
  49. },
  50. }));
  51. const createTestQueryClient = () =>
  52. new QueryClient({
  53. defaultOptions: {
  54. queries: {
  55. retry: false,
  56. },
  57. },
  58. });
  59. const renderWithProviders = (component: React.ReactElement) => {
  60. const queryClient = createTestQueryClient();
  61. return render(
  62. <QueryClientProvider client={queryClient}>
  63. {component as any}
  64. </QueryClientProvider>
  65. );
  66. };
  67. describe('商户管理集成测试', () => {
  68. beforeEach(() => {
  69. vi.clearAllMocks();
  70. });
  71. it('应该完成完整的商户CRUD流程', async () => {
  72. const mockMerchants = {
  73. data: [
  74. {
  75. id: 1,
  76. name: '测试商户',
  77. username: 'testmerchant',
  78. realname: '张三',
  79. phone: '13800138000',
  80. state: 1,
  81. loginNum: 5,
  82. createdAt: '2024-01-01T00:00:00Z',
  83. updatedAt: '2024-01-01T00:00:00Z',
  84. lastLoginTime: 1704067200,
  85. lastLoginIp: '192.168.1.1',
  86. rsaPublicKey: '',
  87. aesKey: '',
  88. },
  89. ],
  90. pagination: {
  91. total: 1,
  92. page: 1,
  93. pageSize: 10,
  94. },
  95. };
  96. const { toast } = await import('sonner');
  97. // Mock initial merchant list
  98. (merchantClientManager.get().index.$get as any).mockResolvedValue(createMockResponse(200, mockMerchants));
  99. renderWithProviders(<MerchantManagement />);
  100. // Wait for initial data to load
  101. await waitFor(() => {
  102. expect(screen.getByText('testmerchant')).toBeInTheDocument();
  103. });
  104. // Test create merchant
  105. const createButton = screen.getByTestId('create-merchant-button');
  106. fireEvent.click(createButton);
  107. // Fill create form
  108. const nameInput = screen.getByPlaceholderText('请输入商户名称');
  109. const usernameInput = screen.getByPlaceholderText('请输入用户名');
  110. const passwordInput = screen.getByPlaceholderText('请输入密码');
  111. const phoneInput = screen.getByPlaceholderText('请输入手机号');
  112. fireEvent.change(nameInput, { target: { value: '新商户' } });
  113. fireEvent.change(usernameInput, { target: { value: 'newmerchant' } });
  114. fireEvent.change(passwordInput, { target: { value: 'password123' } });
  115. fireEvent.change(phoneInput, { target: { value: '13800138000' } });
  116. // Mock successful creation
  117. (merchantClientManager.get().index.$post as any).mockResolvedValue(createMockResponse(201, { id: 2, username: 'newmerchant' }));
  118. const submitButton = screen.getByTestId('create-submit-button');
  119. fireEvent.click(submitButton);
  120. await waitFor(() => {
  121. expect(merchantClientManager.get().index.$post).toHaveBeenCalledWith({
  122. json: {
  123. name: '新商户',
  124. username: 'newmerchant',
  125. password: 'password123',
  126. phone: '13800138000',
  127. realname: '',
  128. state: 2,
  129. rsaPublicKey: '',
  130. aesKey: '',
  131. },
  132. });
  133. expect(toast.success).toHaveBeenCalledWith('商户创建成功');
  134. });
  135. // Test edit merchant
  136. const editButtons = screen.getAllByTestId('edit-button-1');
  137. fireEvent.click(editButtons[0]);
  138. // Verify edit form is populated
  139. await waitFor(() => {
  140. expect(screen.getByDisplayValue('测试商户')).toBeInTheDocument();
  141. });
  142. // Update merchant
  143. const updateNameInput = screen.getByDisplayValue('测试商户');
  144. fireEvent.change(updateNameInput, { target: { value: '更新商户' } });
  145. // Mock successful update
  146. (merchantClientManager.get()[':id']['$put'] as any).mockResolvedValue(createMockResponse(200));
  147. const updateButton = screen.getByTestId('update-submit-button');
  148. fireEvent.click(updateButton);
  149. await waitFor(() => {
  150. expect(merchantClientManager.get()[':id']['$put']).toHaveBeenCalledWith({
  151. param: { id: 1 },
  152. json: {
  153. name: '更新商户',
  154. username: 'testmerchant',
  155. phone: '13800138000',
  156. realname: '张三',
  157. password: undefined,
  158. state: 1,
  159. rsaPublicKey: '',
  160. aesKey: '',
  161. },
  162. });
  163. expect(toast.success).toHaveBeenCalledWith('商户更新成功');
  164. });
  165. // Test delete merchant
  166. const deleteButtons = screen.getAllByTestId('delete-button-1');
  167. fireEvent.click(deleteButtons[0]);
  168. // Confirm deletion
  169. expect(screen.getByText('确认删除')).toBeInTheDocument();
  170. // Mock successful deletion
  171. (merchantClientManager.get()[':id']['$delete'] as any).mockResolvedValue({
  172. status: 204,
  173. });
  174. const confirmDeleteButton = screen.getByText('删除');
  175. fireEvent.click(confirmDeleteButton);
  176. await waitFor(() => {
  177. expect(merchantClientManager.get()[':id']['$delete']).toHaveBeenCalledWith({
  178. param: { id: 1 },
  179. });
  180. expect(toast.success).toHaveBeenCalledWith('商户删除成功');
  181. });
  182. });
  183. it('应该优雅处理API错误', async () => {
  184. const { toast } = await import('sonner');
  185. // Mock API error
  186. (merchantClientManager.get().index.$get as any).mockRejectedValue(new Error('API Error'));
  187. renderWithProviders(<MerchantManagement />);
  188. // Should handle error without crashing
  189. await waitFor(() => {
  190. expect(screen.getByText('商户管理')).toBeInTheDocument();
  191. });
  192. // Test create merchant error
  193. const createButton = screen.getByTestId('create-merchant-button');
  194. fireEvent.click(createButton);
  195. // 等待对话框打开
  196. await waitFor(() => {
  197. expect(screen.getByRole('dialog')).toBeInTheDocument();
  198. });
  199. // 填写必填字段
  200. const nameInput = screen.getByPlaceholderText('请输入商户名称');
  201. const usernameInput = screen.getByPlaceholderText('请输入用户名');
  202. const passwordInput = screen.getByPlaceholderText('请输入密码');
  203. const phoneInput = screen.getByPlaceholderText('请输入手机号');
  204. fireEvent.change(nameInput, { target: { value: '测试商户' } });
  205. fireEvent.change(usernameInput, { target: { value: 'testmerchant' } });
  206. fireEvent.change(passwordInput, { target: { value: 'password123' } });
  207. fireEvent.change(phoneInput, { target: { value: '13800138000' } });
  208. // Mock creation error
  209. (merchantClientManager.get().index.$post as any).mockRejectedValue(new Error('创建商户失败'));
  210. const submitButton = screen.getByTestId('create-submit-button');
  211. fireEvent.click(submitButton);
  212. await waitFor(() => {
  213. expect(toast.error).toHaveBeenCalledWith('创建商户失败');
  214. });
  215. });
  216. it('应该处理搜索功能', async () => {
  217. const mockMerchants = {
  218. data: [],
  219. pagination: { total: 0, page: 1, pageSize: 10 },
  220. };
  221. (merchantClientManager.get().index.$get as any).mockResolvedValue(createMockResponse(200, mockMerchants));
  222. renderWithProviders(<MerchantManagement />);
  223. // 等待数据加载完成
  224. await waitFor(() => {
  225. expect(screen.getByText('商户列表')).toBeInTheDocument();
  226. });
  227. // Test search
  228. const searchInput = screen.getByPlaceholderText('搜索商户名称、用户名、手机号...');
  229. fireEvent.change(searchInput, { target: { value: '搜索关键词' } });
  230. const searchButton = screen.getByTestId('search-button');
  231. fireEvent.click(searchButton);
  232. await waitFor(() => {
  233. expect(merchantClientManager.get().index.$get).toHaveBeenCalledWith({
  234. query: {
  235. page: 1,
  236. pageSize: 10,
  237. keyword: '搜索关键词',
  238. },
  239. });
  240. });
  241. });
  242. it('应该显示商户详情', async () => {
  243. const mockMerchants = {
  244. data: [
  245. {
  246. id: 1,
  247. name: '测试商户',
  248. username: 'testmerchant',
  249. realname: '张三',
  250. phone: '13800138000',
  251. state: 1,
  252. loginNum: 5,
  253. createdAt: '2024-01-01T00:00:00Z',
  254. updatedAt: '2024-01-01T00:00:00Z',
  255. lastLoginTime: 1704067200,
  256. lastLoginIp: '192.168.1.1',
  257. rsaPublicKey: '',
  258. aesKey: '',
  259. },
  260. ],
  261. pagination: {
  262. total: 1,
  263. page: 1,
  264. pageSize: 10,
  265. },
  266. };
  267. (merchantClientManager.get().index.$get as any).mockResolvedValue(createMockResponse(200, mockMerchants));
  268. renderWithProviders(<MerchantManagement />);
  269. // Wait for data to load
  270. await waitFor(() => {
  271. expect(screen.getByText('testmerchant')).toBeInTheDocument();
  272. });
  273. // Test view details
  274. const viewButtons = screen.getAllByTestId('view-detail-button-1');
  275. fireEvent.click(viewButtons[0]);
  276. // Verify detail dialog appears
  277. await waitFor(() => {
  278. expect(screen.getByText('商户详情')).toBeInTheDocument();
  279. });
  280. // 验证详情内容 - 只验证对话框标题
  281. expect(screen.getByText('商户详情')).toBeInTheDocument();
  282. });
  283. it('应该正确处理分页', async () => {
  284. const mockMerchantsPage1 = {
  285. data: [
  286. { id: '1', name: '商户1', username: 'merchant1', phone: '13800000001', status: 'active', createdAt: '2024-01-01T00:00:00Z' },
  287. { id: '2', name: '商户2', username: 'merchant2', phone: '13800000002', status: 'active', createdAt: '2024-01-01T00:00:00Z' }
  288. ],
  289. pagination: { total: 25, page: 1, pageSize: 10 },
  290. };
  291. const mockMerchantsPage2 = {
  292. data: [
  293. { id: '3', name: '商户3', username: 'merchant3', phone: '13800000003', status: 'active', createdAt: '2024-01-01T00:00:00Z' },
  294. { id: '4', name: '商户4', username: 'merchant4', phone: '13800000004', status: 'active', createdAt: '2024-01-01T00:00:00Z' }
  295. ],
  296. pagination: { total: 25, page: 2, pageSize: 10 },
  297. };
  298. // 设置 mock 响应 - 先只用一个响应
  299. (merchantClientManager.get().index.$get as any)
  300. .mockResolvedValue(createMockResponse(200, mockMerchantsPage1));
  301. renderWithProviders(<MerchantManagement />);
  302. // Wait for data to load
  303. await waitFor(() => {
  304. expect(screen.getByText('商户列表')).toBeInTheDocument();
  305. });
  306. // Debug: Check if merchants are displayed
  307. const merchantNames = screen.queryAllByText(/商户[0-9]/);
  308. console.log('Merchant names found:', merchantNames.length);
  309. console.log('Merchant names:', merchantNames.map(el => el.textContent));
  310. // Debug: Check what pagination elements are rendered
  311. const allLinks = screen.getAllByRole('link');
  312. console.log('All links:', allLinks.map(link => ({
  313. text: link.textContent,
  314. ariaLabel: link.getAttribute('aria-label'),
  315. ariaDisabled: link.getAttribute('aria-disabled')
  316. })));
  317. // Debug: Check if pagination info is displayed
  318. const paginationInfo = screen.queryByText(/共.*条记录/);
  319. console.log('Pagination info:', paginationInfo?.textContent);
  320. // Test pagination - click next page
  321. const nextPageButton = screen.getByLabelText(/next/i);
  322. fireEvent.click(nextPageButton);
  323. // 等待分页请求完成 - 增加等待时间
  324. await waitFor(() => {
  325. expect(merchantClientManager.get().index.$get).toHaveBeenCalledTimes(2);
  326. }, { timeout: 5000 });
  327. // 验证第二次调用的参数
  328. const calls = (merchantClientManager.get().index.$get as any).mock.calls;
  329. expect(calls[1][0]).toEqual({
  330. query: {
  331. page: 2,
  332. pageSize: 10,
  333. keyword: '',
  334. },
  335. });
  336. });
  337. });