RouteForm.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  6. import { RouteForm } from '@/client/admin/components/RouteForm';
  7. import { VehicleType } from '@/server/modules/routes/route.schema';
  8. // 创建测试包装器
  9. const TestWrapper = ({ children }: { children: React.ReactNode }) => {
  10. const queryClient = new QueryClient({
  11. defaultOptions: {
  12. queries: {
  13. retry: false,
  14. },
  15. },
  16. });
  17. return (
  18. <QueryClientProvider client={queryClient}>
  19. {children}
  20. </QueryClientProvider>
  21. );
  22. };
  23. // Mock API 客户端
  24. // Mock API 客户端
  25. vi.mock('@/client/api', () => ({
  26. locationClient: {
  27. $get: vi.fn().mockResolvedValue({
  28. status: 200,
  29. ok: true,
  30. json: async () => ({
  31. data: [
  32. {
  33. id: 1,
  34. name: '北京工人体育场',
  35. address: '北京市朝阳区工人体育场北路',
  36. province: {
  37. id: 1,
  38. name: '北京市',
  39. code: '110000'
  40. },
  41. city: {
  42. id: 2,
  43. name: '北京市',
  44. code: '110100'
  45. },
  46. district: {
  47. id: 3,
  48. name: '朝阳区',
  49. code: '110105'
  50. },
  51. latitude: 39.9334,
  52. longitude: 116.4473,
  53. isDisabled: 0,
  54. createdAt: '2024-01-01T00:00:00.000Z',
  55. updatedAt: '2024-01-01T00:00:00.000Z'
  56. },
  57. {
  58. id: 2,
  59. name: '北京鸟巢',
  60. address: '北京市朝阳区国家体育场南路1号',
  61. province: {
  62. id: 1,
  63. name: '北京市',
  64. code: '110000'
  65. },
  66. city: {
  67. id: 2,
  68. name: '北京市',
  69. code: '110100'
  70. },
  71. district: {
  72. id: 3,
  73. name: '朝阳区',
  74. code: '110105'
  75. },
  76. latitude: 39.9929,
  77. longitude: 116.3963,
  78. isDisabled: 0,
  79. createdAt: '2024-01-01T00:00:00.000Z',
  80. updatedAt: '2024-01-01T00:00:00.000Z'
  81. }
  82. ],
  83. pagination: {
  84. current: 1,
  85. pageSize: 100,
  86. total: 2
  87. }
  88. })
  89. })
  90. },
  91. activityClient: {
  92. $get: vi.fn().mockResolvedValue({
  93. status: 200,
  94. ok: true,
  95. json: async () => ({
  96. data: [
  97. {
  98. id: 1,
  99. name: '测试活动',
  100. type: 'departure',
  101. startDate: '2025-10-17T08:00:00.000Z',
  102. endDate: '2025-10-17T18:00:00.000Z',
  103. isDisabled: 0
  104. }
  105. ],
  106. pagination: {
  107. current: 1,
  108. pageSize: 100,
  109. total: 1
  110. }
  111. })
  112. })
  113. }
  114. }));
  115. // Mock ActivitySelect 组件,使其自动返回有效的 activityId
  116. vi.mock('@/client/admin/components/ActivitySelect', () => ({
  117. ActivitySelect: ({ value, onValueChange, placeholder, 'data-testid': dataTestId }: any) => (
  118. <button
  119. data-testid={dataTestId}
  120. onClick={() => onValueChange && onValueChange(1)}
  121. >
  122. {value ? `已选择活动 ${value}` : placeholder}
  123. </button>
  124. )
  125. }));
  126. describe('RouteForm', () => {
  127. const user = userEvent.setup();
  128. const mockOnSubmit = vi.fn();
  129. const mockOnCancel = vi.fn();
  130. beforeEach(() => {
  131. vi.clearAllMocks();
  132. });
  133. it('应该正确渲染创建表单', () => {
  134. render(
  135. <TestWrapper>
  136. <RouteForm
  137. onSubmit={mockOnSubmit}
  138. onCancel={mockOnCancel}
  139. />
  140. </TestWrapper>
  141. );
  142. expect(screen.getByLabelText('路线名称 *')).toBeInTheDocument();
  143. expect(screen.getByText('车型 *')).toBeInTheDocument();
  144. expect(screen.getByText('出发地 *')).toBeInTheDocument();
  145. expect(screen.getByTestId('start-location-select')).toBeInTheDocument();
  146. expect(screen.getByText('目的地 *')).toBeInTheDocument();
  147. expect(screen.getByTestId('end-location-select')).toBeInTheDocument();
  148. expect(screen.getByText('上车点 *')).toBeInTheDocument();
  149. expect(screen.getByText('下车点 *')).toBeInTheDocument();
  150. expect(screen.getByText('出发时间 *')).toBeInTheDocument();
  151. expect(screen.getByText('价格 *')).toBeInTheDocument();
  152. expect(screen.getByText('总座位数 *')).toBeInTheDocument();
  153. expect(screen.getByText('可用座位数 *')).toBeInTheDocument();
  154. expect(screen.getByText('关联活动 *')).toBeInTheDocument();
  155. });
  156. it('应该正确渲染编辑表单', () => {
  157. const initialData = {
  158. id: 1,
  159. name: '测试路线',
  160. description: '测试路线描述',
  161. startLocationId: 1,
  162. endLocationId: 2,
  163. pickupPoint: '测试上车点',
  164. dropoffPoint: '测试下车点',
  165. departureTime: new Date('2025-10-17T08:00:00.000Z'),
  166. vehicleType: VehicleType.BUS,
  167. price: 100,
  168. seatCount: 50,
  169. availableSeats: 45,
  170. activityId: 1,
  171. isDisabled: 0
  172. };
  173. render(
  174. <TestWrapper>
  175. <RouteForm
  176. initialData={initialData}
  177. onSubmit={mockOnSubmit}
  178. onCancel={mockOnCancel}
  179. />
  180. </TestWrapper>
  181. );
  182. expect(screen.getByDisplayValue('测试路线')).toBeInTheDocument();
  183. expect(screen.getByDisplayValue('测试路线描述')).toBeInTheDocument();
  184. expect(screen.getByDisplayValue('测试上车点')).toBeInTheDocument();
  185. expect(screen.getByDisplayValue('测试下车点')).toBeInTheDocument();
  186. expect(screen.getByDisplayValue('100')).toBeInTheDocument();
  187. expect(screen.getByDisplayValue('50')).toBeInTheDocument();
  188. expect(screen.getByDisplayValue('45')).toBeInTheDocument();
  189. });
  190. it('应该能够选择出发地和目的地', async () => {
  191. render(
  192. <TestWrapper>
  193. <RouteForm
  194. onSubmit={mockOnSubmit}
  195. onCancel={mockOnCancel}
  196. />
  197. </TestWrapper>
  198. );
  199. // 点击出发地选择器
  200. const startLocationSelect = screen.getByTestId('start-location-select');
  201. await user.click(startLocationSelect);
  202. // 等待地点数据加载
  203. await waitFor(() => {
  204. expect(screen.getByText('北京工人体育场')).toBeInTheDocument();
  205. expect(screen.getByText('北京鸟巢')).toBeInTheDocument();
  206. });
  207. // 选择出发地 - 使用更具体的选择器
  208. const startLocationOptions = screen.getAllByRole('option');
  209. const startLocationOption = startLocationOptions.find(option =>
  210. option.textContent?.includes('北京工人体育场')
  211. );
  212. expect(startLocationOption).toBeDefined();
  213. await user.click(startLocationOption!);
  214. // 验证出发地被选中
  215. await waitFor(() => {
  216. expect(screen.getByTestId('start-location-select')).toHaveTextContent('北京工人体育场');
  217. });
  218. // 点击目的地选择器
  219. const endLocationSelect = screen.getByTestId('end-location-select');
  220. await user.click(endLocationSelect);
  221. // 等待地点数据加载
  222. await waitFor(() => {
  223. expect(screen.getAllByRole('option').length).toBeGreaterThan(0);
  224. });
  225. // 选择目的地
  226. const endLocationOptions = screen.getAllByRole('option');
  227. const endLocationOption = endLocationOptions.find(option =>
  228. option.textContent?.includes('北京鸟巢')
  229. );
  230. expect(endLocationOption).toBeDefined();
  231. await user.click(endLocationOption!);
  232. // 验证目的地被选中
  233. await waitFor(() => {
  234. expect(screen.getByTestId('end-location-select')).toHaveTextContent('北京鸟巢');
  235. });
  236. });
  237. it('应该验证必填字段', async () => {
  238. render(
  239. <TestWrapper>
  240. <RouteForm
  241. onSubmit={mockOnSubmit}
  242. onCancel={mockOnCancel}
  243. />
  244. </TestWrapper>
  245. );
  246. // 尝试提交空表单
  247. await user.click(screen.getByRole('button', { name: '创建路线' }));
  248. // 验证错误消息
  249. await waitFor(() => {
  250. expect(screen.getByText('路线名称不能为空')).toBeInTheDocument();
  251. });
  252. });
  253. it('应该能够选择出发地和目的地', async () => {
  254. render(
  255. <TestWrapper>
  256. <RouteForm
  257. onSubmit={mockOnSubmit}
  258. onCancel={mockOnCancel}
  259. />
  260. </TestWrapper>
  261. );
  262. // 选择出发地
  263. const startLocationSelect = screen.getByTestId('start-location-select');
  264. await user.click(startLocationSelect);
  265. await waitFor(() => {
  266. expect(screen.getByText('北京工人体育场')).toBeInTheDocument();
  267. });
  268. // 使用更可靠的方法选择地点
  269. const startLocationOptions = screen.getAllByRole('option');
  270. const startLocationOption = startLocationOptions.find(option =>
  271. option.textContent?.includes('北京工人体育场')
  272. );
  273. expect(startLocationOption).toBeDefined();
  274. await user.click(startLocationOption!);
  275. // 验证出发地被选中
  276. await waitFor(() => {
  277. expect(screen.getByTestId('start-location-select')).toHaveTextContent('北京工人体育场');
  278. });
  279. // 选择目的地
  280. const endLocationSelect = screen.getByTestId('end-location-select');
  281. await user.click(endLocationSelect);
  282. await waitFor(() => {
  283. expect(screen.getAllByRole('option').length).toBeGreaterThan(0);
  284. });
  285. const endLocationOptions = screen.getAllByRole('option');
  286. const endLocationOption = endLocationOptions.find(option =>
  287. option.textContent?.includes('北京鸟巢')
  288. );
  289. expect(endLocationOption).toBeDefined();
  290. await user.click(endLocationOption!);
  291. // 验证目的地被选中
  292. await waitFor(() => {
  293. expect(screen.getByTestId('end-location-select')).toHaveTextContent('北京鸟巢');
  294. });
  295. });
  296. it('应该能够取消表单', async () => {
  297. render(
  298. <TestWrapper>
  299. <RouteForm
  300. onSubmit={mockOnSubmit}
  301. onCancel={mockOnCancel}
  302. />
  303. </TestWrapper>
  304. );
  305. await user.click(screen.getByRole('button', { name: '取消' }));
  306. expect(mockOnCancel).toHaveBeenCalled();
  307. });
  308. it('应该在编辑模式下显示状态选择', () => {
  309. const initialData = {
  310. id: 1,
  311. name: '测试路线',
  312. description: '测试路线描述',
  313. startLocationId: 1,
  314. endLocationId: 2,
  315. pickupPoint: '测试上车点',
  316. dropoffPoint: '测试下车点',
  317. departureTime: new Date('2025-10-17T08:00:00.000Z'),
  318. vehicleType: VehicleType.BUS,
  319. price: 100,
  320. seatCount: 50,
  321. availableSeats: 45,
  322. activityId: 1,
  323. isDisabled: 0
  324. };
  325. render(
  326. <TestWrapper>
  327. <RouteForm
  328. initialData={initialData}
  329. onSubmit={mockOnSubmit}
  330. onCancel={mockOnCancel}
  331. />
  332. </TestWrapper>
  333. );
  334. expect(screen.getByLabelText('路线状态')).toBeInTheDocument();
  335. });
  336. });