orders.test.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. import { describe, it, expect, vi, beforeEach } from 'vitest';
  2. import { render, screen, waitFor } from '@testing-library/react';
  3. import userEvent from '@testing-library/user-event';
  4. import '@testing-library/jest-dom';
  5. import { OrdersPage } from '@/client/admin/pages/Orders';
  6. import { TestWrapper } from '~/utils/client/test-render';
  7. // Import mocked modules
  8. import { orderClient } from '@/client/api';
  9. import { toast } from 'sonner';
  10. import { OrderStatus, PaymentStatus } from '@d8d/server/share/order.types';
  11. // Mock API 客户端
  12. vi.mock('@/client/api', () => ({
  13. orderClient: {
  14. $get: vi.fn().mockResolvedValue({
  15. status: 200,
  16. ok: true,
  17. json: async () => ({
  18. data: [
  19. {
  20. id: 1,
  21. userId: 1,
  22. routeId: 1,
  23. passengerCount: 2,
  24. totalAmount: 100.5,
  25. status: OrderStatus.PENDING_PAYMENT,
  26. paymentStatus: PaymentStatus.PENDING,
  27. passengerSnapshots: [
  28. { name: '张三', idCard: '123456789012345678', phone: '13800138000' },
  29. { name: '李四', idCard: '123456789012345679', phone: '13800138001' }
  30. ],
  31. routeSnapshot: { name: '测试路线', description: '测试路线描述' },
  32. createdBy: 1,
  33. updatedBy: null,
  34. createdAt: '2024-01-01T00:00:00.000Z',
  35. updatedAt: '2024-01-01T00:00:00.000Z',
  36. user: {
  37. id: 1,
  38. username: 'testuser',
  39. phone: '13800138000'
  40. },
  41. route: {
  42. id: 1,
  43. name: '测试路线',
  44. description: '测试路线描述'
  45. }
  46. }
  47. ],
  48. total: 1,
  49. page: 1,
  50. pageSize: 10
  51. })
  52. }),
  53. stats: {
  54. $get: vi.fn().mockResolvedValue({
  55. status: 200,
  56. ok: true,
  57. json: async () => ({
  58. total: 10,
  59. pendingPayment: 2,
  60. waitingDeparture: 3,
  61. inProgress: 1,
  62. completed: 3,
  63. cancelled: 1
  64. })
  65. })
  66. }
  67. }
  68. }));
  69. // Mock toast
  70. vi.mock('sonner', () => ({
  71. toast: {
  72. success: vi.fn(),
  73. error: vi.fn(),
  74. info: vi.fn(),
  75. warning: vi.fn(),
  76. }
  77. }));
  78. // Mock xlsx
  79. vi.mock('xlsx', () => ({
  80. utils: {
  81. book_new: vi.fn(() => ({})),
  82. json_to_sheet: vi.fn(() => ({})),
  83. book_append_sheet: vi.fn()
  84. },
  85. writeFile: vi.fn()
  86. }));
  87. describe('OrdersPage 集成测试', () => {
  88. const user = userEvent.setup();
  89. beforeEach(() => {
  90. vi.clearAllMocks();
  91. });
  92. it('应该正确渲染订单管理页面标题', async () => {
  93. render(
  94. <TestWrapper>
  95. <OrdersPage />
  96. </TestWrapper>
  97. );
  98. expect(screen.getByText('订单管理')).toBeInTheDocument();
  99. });
  100. it('应该显示订单统计面板', async () => {
  101. render(
  102. <TestWrapper>
  103. <OrdersPage />
  104. </TestWrapper>
  105. );
  106. // 等待数据加载
  107. await waitFor(() => {
  108. expect(screen.getByTestId('total-orders-count')).toHaveTextContent('10'); // 总订单数
  109. expect(screen.getByTestId('pending-payment-count')).toHaveTextContent('2'); // 待支付数量
  110. expect(screen.getByTestId('waiting-departure-count')).toHaveTextContent('3'); // 待出发数量
  111. expect(screen.getByTestId('in-progress-count')).toHaveTextContent('1'); // 行程中数量
  112. expect(screen.getByTestId('completed-count')).toHaveTextContent('3'); // 已完成数量
  113. expect(screen.getByTestId('cancelled-count')).toHaveTextContent('1'); // 已取消数量
  114. });
  115. });
  116. it('应该显示订单列表和搜索功能', async () => {
  117. render(
  118. <TestWrapper>
  119. <OrdersPage />
  120. </TestWrapper>
  121. );
  122. // 等待数据加载
  123. await waitFor(() => {
  124. expect(screen.getByPlaceholderText('搜索订单号、用户信息...')).toBeInTheDocument();
  125. });
  126. expect(screen.getByText('搜索')).toBeInTheDocument();
  127. expect(screen.getByText('高级筛选')).toBeInTheDocument();
  128. });
  129. it('应该处理搜索功能', async () => {
  130. render(
  131. <TestWrapper>
  132. <OrdersPage />
  133. </TestWrapper>
  134. );
  135. const searchInput = screen.getByPlaceholderText('搜索订单号、用户信息...');
  136. const searchButton = screen.getByText('搜索');
  137. // 输入搜索关键词
  138. await user.type(searchInput, 'testuser');
  139. await user.click(searchButton);
  140. // 验证搜索参数被设置
  141. expect(searchInput).toHaveValue('testuser');
  142. });
  143. it('应该显示高级筛选功能', async () => {
  144. render(
  145. <TestWrapper>
  146. <OrdersPage />
  147. </TestWrapper>
  148. );
  149. const filterButton = screen.getByRole('button', { name: '高级筛选' });
  150. await user.click(filterButton);
  151. // 验证筛选表单显示
  152. expect(screen.getByText('订单状态')).toBeInTheDocument();
  153. expect(screen.getByText('支付状态')).toBeInTheDocument();
  154. });
  155. it('应该处理订单状态筛选', async () => {
  156. render(
  157. <TestWrapper>
  158. <OrdersPage />
  159. </TestWrapper>
  160. );
  161. const filterButton = screen.getByRole('button', { name: '高级筛选' });
  162. await user.click(filterButton);
  163. // 验证筛选表单显示和状态筛选标签
  164. expect(screen.getByText('订单状态')).toBeInTheDocument();
  165. // 验证状态筛选器存在(通过查找Select组件)
  166. const selectElements = document.querySelectorAll('[role="combobox"]');
  167. expect(selectElements.length).toBeGreaterThan(0);
  168. });
  169. it('应该显示分页组件', async () => {
  170. render(
  171. <TestWrapper>
  172. <OrdersPage />
  173. </TestWrapper>
  174. );
  175. // 验证分页控件存在
  176. await waitFor(() => {
  177. expect(screen.getByText(/共 \d+ 个订单/)).toBeInTheDocument();
  178. });
  179. });
  180. it('应该处理表格数据加载状态', async () => {
  181. render(
  182. <TestWrapper>
  183. <OrdersPage />
  184. </TestWrapper>
  185. );
  186. // 验证骨架屏或加载状态
  187. const skeletonElements = document.querySelectorAll('[data-slot="skeleton"]');
  188. expect(skeletonElements.length).toBeGreaterThan(0);
  189. // 等待数据加载完成
  190. await waitFor(() => {
  191. expect(screen.getByText('testuser')).toBeInTheDocument();
  192. });
  193. });
  194. it('应该显示正确的表格列标题', async () => {
  195. render(
  196. <TestWrapper>
  197. <OrdersPage />
  198. </TestWrapper>
  199. );
  200. // 等待数据加载
  201. await waitFor(() => {
  202. expect(screen.getByText('订单号')).toBeInTheDocument();
  203. expect(screen.getByText('用户')).toBeInTheDocument();
  204. expect(screen.getByText('路线')).toBeInTheDocument();
  205. expect(screen.getByText('乘客数量')).toBeInTheDocument();
  206. expect(screen.getByText('订单金额')).toBeInTheDocument();
  207. expect(screen.getByText('订单状态')).toBeInTheDocument();
  208. expect(screen.getByText('支付状态')).toBeInTheDocument();
  209. expect(screen.getByText('创建时间')).toBeInTheDocument();
  210. expect(screen.getByText('操作')).toBeInTheDocument();
  211. });
  212. });
  213. it('应该包含查看详情操作按钮', async () => {
  214. const { container } = render(
  215. <TestWrapper>
  216. <OrdersPage />
  217. </TestWrapper>
  218. );
  219. // 等待数据加载完成
  220. await waitFor(() => {
  221. expect(screen.getByText('testuser')).toBeInTheDocument();
  222. // 查找操作按钮(通过按钮元素)
  223. const actionButtons = container.querySelectorAll('button');
  224. const hasViewButtons = Array.from(actionButtons).some(button =>
  225. button.innerHTML.includes('eye')
  226. );
  227. expect(hasViewButtons).toBe(true);
  228. });
  229. });
  230. it('应该打开订单详情对话框', async () => {
  231. render(
  232. <TestWrapper>
  233. <OrdersPage />
  234. </TestWrapper>
  235. );
  236. // 等待数据加载
  237. await waitFor(() => {
  238. expect(screen.getByText('testuser')).toBeInTheDocument();
  239. });
  240. // 查找查看详情按钮
  241. const viewButtons = screen.getAllByRole('button').filter(btn =>
  242. btn.innerHTML.includes('eye')
  243. );
  244. if (viewButtons.length > 0) {
  245. await user.click(viewButtons[0]);
  246. // 验证详情对话框打开
  247. await waitFor(() => {
  248. expect(screen.getByRole('heading', { name: '订单详情' })).toBeInTheDocument();
  249. });
  250. }
  251. });
  252. it('应该在详情对话框中显示订单信息', async () => {
  253. render(
  254. <TestWrapper>
  255. <OrdersPage />
  256. </TestWrapper>
  257. );
  258. // 等待数据加载
  259. await waitFor(() => {
  260. expect(screen.getByText('testuser')).toBeInTheDocument();
  261. });
  262. // 查找查看详情按钮
  263. const viewButtons = screen.getAllByRole('button').filter(btn =>
  264. btn.innerHTML.includes('eye')
  265. );
  266. if (viewButtons.length > 0) {
  267. await user.click(viewButtons[0]);
  268. // 验证详情对话框内容
  269. await waitFor(() => {
  270. expect(screen.getByText('订单信息')).toBeInTheDocument();
  271. expect(screen.getByText('用户信息')).toBeInTheDocument();
  272. expect(screen.getByText('路线信息')).toBeInTheDocument();
  273. expect(screen.getByText('订单详情')).toBeInTheDocument();
  274. expect(screen.getByText('乘客信息')).toBeInTheDocument();
  275. // 验证具体数据
  276. expect(screen.getByText('#1')).toBeInTheDocument(); // 订单号
  277. expect(screen.getByText('待支付')).toBeInTheDocument(); // 订单状态
  278. expect(screen.getByText('testuser')).toBeInTheDocument(); // 用户名
  279. expect(screen.getByText('测试路线')).toBeInTheDocument(); // 路线名称
  280. expect(screen.getByText('2')).toBeInTheDocument(); // 乘客数量
  281. expect(screen.getByText('¥100.5')).toBeInTheDocument(); // 订单金额
  282. });
  283. }
  284. });
  285. it('应该处理API错误场景', async () => {
  286. // 模拟API错误
  287. (orderClient.$get as any).mockResolvedValueOnce({
  288. status: 500,
  289. ok: false,
  290. json: async () => ({ error: 'Internal server error' })
  291. });
  292. render(
  293. <TestWrapper>
  294. <OrdersPage />
  295. </TestWrapper>
  296. );
  297. // 验证页面仍然渲染基本结构
  298. expect(screen.getByText('订单管理')).toBeInTheDocument();
  299. // 验证错误处理(组件应该优雅处理错误)
  300. await waitFor(() => {
  301. expect(screen.queryByText('testuser')).not.toBeInTheDocument();
  302. });
  303. });
  304. it('应该处理响应式布局', async () => {
  305. const { container } = render(
  306. <TestWrapper>
  307. <OrdersPage />
  308. </TestWrapper>
  309. );
  310. // 等待数据加载
  311. await waitFor(() => {
  312. expect(screen.getByText('testuser')).toBeInTheDocument();
  313. });
  314. // 展开筛选表单以显示响应式网格
  315. const filterButton = screen.getByRole('button', { name: '高级筛选' });
  316. await user.click(filterButton);
  317. // 验证响应式网格类名
  318. const gridElements = container.querySelectorAll('.grid');
  319. expect(gridElements.length).toBeGreaterThan(0);
  320. // 验证响应式类名存在
  321. const hasResponsiveClasses = container.innerHTML.includes('md:grid-cols-2');
  322. expect(hasResponsiveClasses).toBe(true);
  323. });
  324. it('应该显示订单总数信息', async () => {
  325. render(
  326. <TestWrapper>
  327. <OrdersPage />
  328. </TestWrapper>
  329. );
  330. // 验证订单总数显示
  331. await waitFor(() => {
  332. expect(screen.getByText(/共 \d+ 个订单/)).toBeInTheDocument();
  333. });
  334. });
  335. it('应该处理统计API错误场景', async () => {
  336. // 模拟统计API错误
  337. (orderClient.stats.$get as any).mockResolvedValueOnce({
  338. status: 500,
  339. ok: false,
  340. json: async () => ({ error: 'Internal server error' })
  341. });
  342. render(
  343. <TestWrapper>
  344. <OrdersPage />
  345. </TestWrapper>
  346. );
  347. // 验证页面仍然渲染基本结构
  348. expect(screen.getByText('订单管理')).toBeInTheDocument();
  349. // 验证统计面板显示默认值
  350. await waitFor(() => {
  351. expect(screen.getByText('总订单数')).toBeInTheDocument();
  352. expect(screen.getByText('0')).toBeInTheDocument(); // 默认值
  353. });
  354. });
  355. it('应该处理防抖搜索功能', async () => {
  356. render(
  357. <TestWrapper>
  358. <OrdersPage />
  359. </TestWrapper>
  360. );
  361. const searchInput = screen.getByPlaceholderText('搜索订单号、用户信息...');
  362. // 快速输入多个字符
  363. await user.type(searchInput, 'test');
  364. await user.type(searchInput, 'user');
  365. // 验证输入值正确
  366. expect(searchInput).toHaveValue('testuser');
  367. // 等待防抖延迟
  368. await waitFor(() => {
  369. expect(orderClient.$get).toHaveBeenCalled();
  370. }, { timeout: 500 });
  371. });
  372. it('应该处理筛选条件重置', async () => {
  373. render(
  374. <TestWrapper>
  375. <OrdersPage />
  376. </TestWrapper>
  377. );
  378. // 打开筛选
  379. const filterButton = screen.getByRole('button', { name: '高级筛选' });
  380. await user.click(filterButton);
  381. // 设置筛选条件
  382. const statusSelect = document.querySelectorAll('[role="combobox"]')[0];
  383. await user.click(statusSelect);
  384. // 选择订单状态
  385. const statusOption = screen.getByText('待支付');
  386. await user.click(statusOption);
  387. // 验证重置按钮显示
  388. const resetButton = screen.getByRole('button', { name: '重置' });
  389. expect(resetButton).toBeInTheDocument();
  390. // 点击重置
  391. await user.click(resetButton);
  392. // 验证筛选条件被重置
  393. expect(screen.queryByText('待支付')).not.toBeInTheDocument();
  394. });
  395. it('应该显示导出Excel按钮', async () => {
  396. render(
  397. <TestWrapper>
  398. <OrdersPage />
  399. </TestWrapper>
  400. );
  401. // 验证导出按钮存在
  402. await waitFor(() => {
  403. expect(screen.getByRole('button', { name: '导出Excel' })).toBeInTheDocument();
  404. });
  405. });
  406. it('应该处理导出订单数据成功场景', async () => {
  407. render(
  408. <TestWrapper>
  409. <OrdersPage />
  410. </TestWrapper>
  411. );
  412. // 等待数据加载
  413. await waitFor(() => {
  414. expect(screen.getByRole('button', { name: '导出Excel' })).toBeInTheDocument();
  415. });
  416. const exportButton = screen.getByRole('button', { name: '导出Excel' });
  417. await user.click(exportButton);
  418. // 验证导出过程被调用
  419. await waitFor(() => {
  420. expect(orderClient.$get).toHaveBeenCalledWith({
  421. query: {
  422. page: 1,
  423. pageSize: 10000,
  424. keyword: '',
  425. filters: JSON.stringify({
  426. status: undefined,
  427. paymentStatus: undefined
  428. })
  429. }
  430. });
  431. expect(toast.success).toHaveBeenCalledWith('成功导出 1 条订单数据');
  432. });
  433. });
  434. it('应该处理导出订单数据空数据场景', async () => {
  435. // 模拟空数据响应
  436. (orderClient.$get as any).mockResolvedValueOnce({
  437. status: 200,
  438. ok: true,
  439. json: async () => ({
  440. data: [],
  441. total: 0,
  442. page: 1,
  443. pageSize: 10000
  444. })
  445. });
  446. render(
  447. <TestWrapper>
  448. <OrdersPage />
  449. </TestWrapper>
  450. );
  451. // 等待数据加载
  452. await waitFor(() => {
  453. expect(screen.getByRole('button', { name: '导出Excel' })).toBeInTheDocument();
  454. });
  455. const exportButton = screen.getByRole('button', { name: '导出Excel' });
  456. await user.click(exportButton);
  457. // 验证空数据提示
  458. await waitFor(() => {
  459. expect(toast.warning).toHaveBeenCalledWith('没有找到符合条件的订单数据');
  460. });
  461. });
  462. it('应该处理导出订单数据API错误场景', async () => {
  463. // 模拟API错误
  464. (orderClient.$get as any).mockResolvedValueOnce({
  465. status: 500,
  466. ok: false,
  467. json: async () => ({ error: 'Internal server error' })
  468. });
  469. render(
  470. <TestWrapper>
  471. <OrdersPage />
  472. </TestWrapper>
  473. );
  474. // 等待数据加载
  475. await waitFor(() => {
  476. expect(screen.getByRole('button', { name: '导出Excel' })).toBeInTheDocument();
  477. });
  478. const exportButton = screen.getByRole('button', { name: '导出Excel' });
  479. await user.click(exportButton);
  480. // 验证错误处理
  481. await waitFor(() => {
  482. expect(toast.error).toHaveBeenCalledWith('导出订单数据失败,请稍后重试');
  483. });
  484. });
  485. it('应该在导出过程中禁用导出按钮', async () => {
  486. // 模拟延迟响应
  487. (orderClient.$get as any).mockImplementationOnce(() =>
  488. new Promise(resolve => setTimeout(() => resolve({
  489. status: 200,
  490. ok: true,
  491. json: async () => ({
  492. data: [
  493. {
  494. id: 1,
  495. userId: 1,
  496. routeId: 1,
  497. passengerCount: 2,
  498. totalAmount: 100.5,
  499. status: OrderStatus.PENDING_PAYMENT,
  500. paymentStatus: PaymentStatus.PENDING,
  501. passengerSnapshots: [],
  502. routeSnapshot: {},
  503. createdBy: 1,
  504. updatedBy: null,
  505. createdAt: '2024-01-01T00:00:00.000Z',
  506. updatedAt: '2024-01-01T00:00:00.000Z',
  507. user: { id: 1, username: 'testuser', phone: '13800138000' },
  508. route: { id: 1, name: '测试路线', description: '测试路线描述' }
  509. }
  510. ],
  511. total: 1,
  512. page: 1,
  513. pageSize: 10000
  514. })
  515. }), 100))
  516. );
  517. render(
  518. <TestWrapper>
  519. <OrdersPage />
  520. </TestWrapper>
  521. );
  522. // 等待数据加载
  523. await waitFor(() => {
  524. expect(screen.getByRole('button', { name: '导出Excel' })).toBeInTheDocument();
  525. });
  526. const exportButton = screen.getByRole('button', { name: '导出Excel' });
  527. await user.click(exportButton);
  528. // 验证按钮被禁用
  529. expect(exportButton).toBeDisabled();
  530. expect(exportButton).toHaveTextContent('导出中...');
  531. // 等待导出完成
  532. await waitFor(() => {
  533. expect(exportButton).not.toBeDisabled();
  534. expect(exportButton).toHaveTextContent('导出Excel');
  535. }, { timeout: 500 });
  536. });
  537. it('应该包含当前筛选条件在导出请求中', async () => {
  538. render(
  539. <TestWrapper>
  540. <OrdersPage />
  541. </TestWrapper>
  542. );
  543. // 等待数据加载
  544. await waitFor(() => {
  545. expect(screen.getByRole('button', { name: '导出Excel' })).toBeInTheDocument();
  546. });
  547. // 设置搜索条件
  548. const searchInput = screen.getByPlaceholderText('搜索订单号、用户信息...');
  549. await user.type(searchInput, 'testuser');
  550. // 设置筛选条件
  551. const filterButton = screen.getByRole('button', { name: '高级筛选' });
  552. await user.click(filterButton);
  553. // 选择订单状态
  554. const statusSelect = document.querySelectorAll('[role="combobox"]')[0];
  555. await user.click(statusSelect);
  556. const statusOption = screen.getByText('待支付');
  557. await user.click(statusOption);
  558. // 点击导出
  559. const exportButton = screen.getByRole('button', { name: '导出Excel' });
  560. await user.click(exportButton);
  561. // 验证导出请求包含筛选条件
  562. await waitFor(() => {
  563. expect(orderClient.$get).toHaveBeenCalledWith({
  564. query: {
  565. page: 1,
  566. pageSize: 10000,
  567. keyword: 'testuser',
  568. filters: JSON.stringify({
  569. status: OrderStatus.PENDING_PAYMENT,
  570. paymentStatus: undefined
  571. })
  572. }
  573. });
  574. });
  575. });
  576. });