ScheduleListPage.test.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. import React from 'react'
  2. import { render, screen, waitFor } from '@testing-library/react'
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  4. import ScheduleListPage from '../../src/pages/schedule-list/ScheduleListPage'
  5. // Mock Taro API
  6. jest.mock('@tarojs/taro', () => ({
  7. useRouter: jest.fn(() => ({
  8. params: {
  9. startAreaIds: JSON.stringify([1, 2]),
  10. endAreaIds: JSON.stringify([3, 4]),
  11. startAreaName: '',
  12. endAreaName: '',
  13. date: '2025-10-31',
  14. vehicleType: 'bus',
  15. travelMode: 'carpool',
  16. activityId: '1',
  17. routeType: 'departure'
  18. }
  19. })),
  20. navigateTo: jest.fn(),
  21. navigateBack: jest.fn()
  22. }))
  23. // Mock API client
  24. jest.mock('../../src/api', () => ({
  25. routeClient: {
  26. search: {
  27. $get: jest.fn(() => Promise.resolve({
  28. status: 200,
  29. json: () => Promise.resolve({
  30. data: {
  31. routes: [
  32. {
  33. id: 1,
  34. name: '测试路线',
  35. description: null,
  36. startLocationId: 1,
  37. endLocationId: 2,
  38. startLocation: {
  39. id: 1,
  40. name: '起点',
  41. provinceId: 1,
  42. cityId: 1,
  43. districtId: 1,
  44. address: '起点地址'
  45. },
  46. endLocation: {
  47. id: 2,
  48. name: '终点',
  49. provinceId: 2,
  50. cityId: 2,
  51. districtId: 2,
  52. address: '终点地址'
  53. },
  54. pickupPoint: '上车点',
  55. dropoffPoint: '下车点',
  56. departureTime: '2025-10-31T08:00:00Z',
  57. vehicleType: 'bus',
  58. travelMode: 'carpool',
  59. price: 100,
  60. seatCount: 40,
  61. availableSeats: 20,
  62. activityId: 1,
  63. activity: {
  64. id: 1,
  65. name: '测试活动',
  66. description: null,
  67. venueLocationId: 3,
  68. venueLocation: {
  69. id: 3,
  70. name: '活动场地',
  71. provinceId: 3,
  72. cityId: 3,
  73. districtId: 3,
  74. address: '活动场地地址'
  75. },
  76. startDate: '2025-10-31',
  77. endDate: '2025-11-01'
  78. },
  79. routeType: 'departure',
  80. isDisabled: 0,
  81. isDeleted: 0,
  82. createdAt: '2025-10-31T00:00:00Z',
  83. updatedAt: '2025-10-31T00:00:00Z'
  84. }
  85. ],
  86. activities: [
  87. {
  88. id: 1,
  89. name: '测试活动',
  90. description: null,
  91. venueLocationId: 3,
  92. venueLocation: {
  93. id: 3,
  94. name: '活动场地',
  95. provinceId: 3,
  96. cityId: 3,
  97. districtId: 3,
  98. address: '活动场地地址'
  99. },
  100. startDate: '2025-10-31',
  101. endDate: '2025-11-01'
  102. }
  103. ]
  104. }
  105. })
  106. }))
  107. }
  108. }
  109. }))
  110. // Mock date-fns
  111. jest.mock('date-fns', () => ({
  112. format: jest.fn(() => '08:00'),
  113. zhCN: {}
  114. }))
  115. // Mock components
  116. jest.mock('../../src/components/ui/navbar', () => ({
  117. Navbar: ({ title, onClickLeft }: { title: string; onClickLeft: () => void }) => (
  118. <div data-testid="navbar">
  119. <div>{title}</div>
  120. <button onClick={onClickLeft}>返回</button>
  121. </div>
  122. ),
  123. NavbarPresets: {
  124. primary: {}
  125. }
  126. }))
  127. const createTestQueryClient = () => new QueryClient({
  128. defaultOptions: {
  129. queries: { retry: false },
  130. },
  131. })
  132. const Wrapper = ({ children }: { children: React.ReactNode }) => (
  133. <QueryClientProvider client={createTestQueryClient()}>
  134. {children}
  135. </QueryClientProvider>
  136. )
  137. describe('ScheduleListPage', () => {
  138. beforeEach(() => {
  139. jest.clearAllMocks()
  140. })
  141. test('应该正确渲染班次页面', async () => {
  142. render(
  143. <Wrapper>
  144. <ScheduleListPage />
  145. </Wrapper>
  146. )
  147. // 等待数据加载
  148. await waitFor(() => {
  149. expect(screen.getByText('选择班次')).toBeInTheDocument()
  150. })
  151. // 验证页面标题
  152. expect(screen.getByText('选择班次')).toBeInTheDocument()
  153. // 验证活动名称显示
  154. expect(screen.getByText('测试活动')).toBeInTheDocument()
  155. // 验证路线信息显示 - 起点使用地点名称,终点保持不变
  156. expect(screen.getByText('起点 → 终点')).toBeInTheDocument()
  157. expect(screen.getByText('去程')).toBeInTheDocument()
  158. // 验证班次列表显示
  159. expect(screen.getByText('可选班次')).toBeInTheDocument()
  160. expect(screen.getByText('(1个班次)')).toBeInTheDocument()
  161. // 验证班次详情显示
  162. expect(screen.getByText('08:00')).toBeInTheDocument()
  163. expect(screen.getByText('¥100/人')).toBeInTheDocument()
  164. expect(screen.getByText('大巴拼车')).toBeInTheDocument()
  165. expect(screen.getByText('拼车')).toBeInTheDocument()
  166. expect(screen.getByText('上车点')).toBeInTheDocument()
  167. expect(screen.getByText('下车点')).toBeInTheDocument()
  168. expect(screen.getByText('剩余20/40座')).toBeInTheDocument()
  169. expect(screen.getByText('空调')).toBeInTheDocument()
  170. expect(screen.getByText('免费WiFi')).toBeInTheDocument()
  171. // 验证预订按钮
  172. expect(screen.getByText('立即购票')).toBeInTheDocument()
  173. })
  174. test('应该隐藏日期选择功能', async () => {
  175. render(
  176. <Wrapper>
  177. <ScheduleListPage />
  178. </Wrapper>
  179. )
  180. // 等待数据加载
  181. await waitFor(() => {
  182. expect(screen.getByText('选择班次')).toBeInTheDocument()
  183. })
  184. // 验证日期选择UI组件已隐藏
  185. // 不应该显示"选择出发日期"文本
  186. const dateSelectionText = screen.queryByText('选择出发日期')
  187. expect(dateSelectionText).not.toBeInTheDocument()
  188. // 不应该显示日期选项
  189. const dateOptions = screen.queryByText('2025-10-31')
  190. expect(dateOptions).not.toBeInTheDocument()
  191. })
  192. test('应该显示加载状态', () => {
  193. // Mock loading state
  194. const mockApi = require('../../src/api')
  195. mockApi.routeClient.search.$get.mockImplementationOnce(
  196. () => new Promise(() => {}) // Never resolves to simulate loading
  197. )
  198. render(
  199. <Wrapper>
  200. <ScheduleListPage />
  201. </Wrapper>
  202. )
  203. expect(screen.getByText('加载中...')).toBeInTheDocument()
  204. })
  205. test('应该显示无班次状态', async () => {
  206. // Mock empty data
  207. const mockApi = require('../../src/api')
  208. mockApi.routeClient.search.$get.mockResolvedValueOnce({
  209. status: 200,
  210. json: () => Promise.resolve({
  211. data: {
  212. routes: [],
  213. activities: []
  214. }
  215. })
  216. })
  217. render(
  218. <Wrapper>
  219. <ScheduleListPage />
  220. </Wrapper>
  221. )
  222. await waitFor(() => {
  223. expect(screen.getByText('暂无班次')).toBeInTheDocument()
  224. })
  225. expect(screen.getByText('当前选择的路线暂无可用班次,请返回重新选择路线')).toBeInTheDocument()
  226. })
  227. test('应该在没有省市区名称时使用默认值', async () => {
  228. // Mock router without area names
  229. const mockTaro = require('@tarojs/taro')
  230. mockTaro.useRouter.mockImplementationOnce(() => ({
  231. params: {
  232. startAreaIds: JSON.stringify([1, 2]),
  233. endAreaIds: JSON.stringify([3, 4]),
  234. date: '2025-10-31',
  235. vehicleType: 'bus',
  236. travelMode: 'carpool',
  237. activityId: '1',
  238. routeType: 'departure'
  239. }
  240. }))
  241. render(
  242. <Wrapper>
  243. <ScheduleListPage />
  244. </Wrapper>
  245. )
  246. await waitFor(() => {
  247. expect(screen.getByText('选择班次')).toBeInTheDocument()
  248. })
  249. // 应该显示路线数据中的起点名称和终点名称
  250. expect(screen.getByText('起点 → 终点')).toBeInTheDocument()
  251. })
  252. test('应该优先使用路由参数中的省市区名称', async () => {
  253. // Mock router with custom area names
  254. const mockTaro = require('@tarojs/taro')
  255. mockTaro.useRouter.mockImplementation(() => ({
  256. params: {
  257. startAreaIds: JSON.stringify([1, 2]),
  258. endAreaIds: JSON.stringify([3, 4]),
  259. startAreaName: encodeURIComponent('北京市 朝阳区'),
  260. date: '2025-10-31',
  261. vehicleType: 'bus',
  262. travelMode: 'carpool',
  263. activityId: '1',
  264. routeType: 'departure'
  265. }
  266. }))
  267. // Mock empty route data
  268. const mockApi = require('../../src/api')
  269. mockApi.routeClient.search.$get.mockImplementation(() => Promise.resolve({
  270. status: 200,
  271. json: () => Promise.resolve({
  272. data: {
  273. routes: [],
  274. activities: []
  275. }
  276. })
  277. }))
  278. render(
  279. <Wrapper>
  280. <ScheduleListPage />
  281. </Wrapper>
  282. )
  283. await waitFor(() => {
  284. expect(screen.getByText('选择班次')).toBeInTheDocument()
  285. })
  286. // 等待数据加载完成,确保显示正确的内容
  287. await waitFor(() => {
  288. expect(screen.getByText('暂无班次')).toBeInTheDocument()
  289. })
  290. // 检查地区名称显示 - 等待组件渲染完成
  291. await waitFor(() => {
  292. const areaDisplay = screen.getByTestId('area-display')
  293. expect(areaDisplay).toHaveTextContent('请选择路线 → 请选择路线')
  294. }, { timeout: 5000 })
  295. })
  296. test('应该在没有路线数据时显示默认地区信息', async () => {
  297. // Mock empty data and no area names
  298. const mockTaro = require('@tarojs/taro')
  299. mockTaro.useRouter.mockImplementationOnce(() => ({
  300. params: {
  301. startAreaIds: JSON.stringify([1, 2]),
  302. endAreaIds: JSON.stringify([3, 4]),
  303. date: '2025-10-31',
  304. vehicleType: 'bus',
  305. travelMode: 'carpool',
  306. activityId: '1',
  307. routeType: 'departure'
  308. }
  309. }))
  310. const mockApi = require('../../src/api')
  311. mockApi.routeClient.search.$get.mockResolvedValueOnce({
  312. status: 200,
  313. json: () => Promise.resolve({
  314. data: {
  315. routes: [],
  316. activities: []
  317. }
  318. })
  319. })
  320. render(
  321. <Wrapper>
  322. <ScheduleListPage />
  323. </Wrapper>
  324. )
  325. await waitFor(() => {
  326. expect(screen.getByText('暂无班次')).toBeInTheDocument()
  327. })
  328. // 应该显示默认的起点名称和终点名称
  329. expect(screen.getByText('请选择路线 → 请选择路线')).toBeInTheDocument()
  330. })
  331. test('去程路线应该优化起点显示为省市区格式', async () => {
  332. // Mock router with area names for departure route
  333. const mockTaro = require('@tarojs/taro')
  334. mockTaro.useRouter.mockImplementation(() => ({
  335. params: {
  336. startAreaIds: JSON.stringify([1, 2]),
  337. endAreaIds: JSON.stringify([3, 4]),
  338. startAreaName: encodeURIComponent('广东省 广州市 市辖区'),
  339. endAreaName: '',
  340. date: '2025-10-31',
  341. vehicleType: 'bus',
  342. travelMode: 'carpool',
  343. activityId: '1',
  344. routeType: 'departure'
  345. }
  346. }))
  347. // Mock API to return departure route data
  348. const mockApi = require('../../src/api')
  349. mockApi.routeClient.search.$get.mockImplementation(() => Promise.resolve({
  350. status: 200,
  351. json: () => Promise.resolve({
  352. data: {
  353. routes: [
  354. {
  355. id: 1,
  356. name: '测试路线',
  357. description: null,
  358. startLocationId: 1,
  359. endLocationId: 2,
  360. startLocation: {
  361. id: 1,
  362. name: '起点',
  363. provinceId: 1,
  364. cityId: 1,
  365. districtId: 1,
  366. address: '起点地址'
  367. },
  368. endLocation: {
  369. id: 2,
  370. name: '终点',
  371. provinceId: 2,
  372. cityId: 2,
  373. districtId: 2,
  374. address: '终点地址'
  375. },
  376. pickupPoint: '上车点',
  377. dropoffPoint: '下车点',
  378. departureTime: '2025-10-31T08:00:00Z',
  379. vehicleType: 'bus',
  380. travelMode: 'carpool',
  381. price: 100,
  382. seatCount: 40,
  383. availableSeats: 20,
  384. activityId: 1,
  385. activity: {
  386. id: 1,
  387. name: '测试活动',
  388. description: null,
  389. venueLocationId: 3,
  390. venueLocation: {
  391. id: 3,
  392. name: '活动场地',
  393. provinceId: 3,
  394. cityId: 3,
  395. districtId: 3,
  396. address: '活动场地地址'
  397. },
  398. startDate: '2025-10-31',
  399. endDate: '2025-11-01'
  400. },
  401. routeType: 'departure',
  402. isDisabled: 0,
  403. isDeleted: 0,
  404. createdAt: '2025-10-31T00:00:00Z',
  405. updatedAt: '2025-10-31T00:00:00Z'
  406. }
  407. ],
  408. activities: [
  409. {
  410. id: 1,
  411. name: '测试活动',
  412. description: null,
  413. venueLocationId: 3,
  414. venueLocation: {
  415. id: 3,
  416. name: '活动场地',
  417. provinceId: 3,
  418. cityId: 3,
  419. districtId: 3,
  420. address: '活动场地地址'
  421. },
  422. startDate: '2025-10-31',
  423. endDate: '2025-11-01'
  424. }
  425. ]
  426. }
  427. })
  428. }))
  429. render(
  430. <Wrapper>
  431. <ScheduleListPage />
  432. </Wrapper>
  433. )
  434. await waitFor(() => {
  435. expect(screen.getByText('选择班次')).toBeInTheDocument()
  436. })
  437. // 去程路线:应该显示"搜索起点 省份 城市 区县 → 路线终点地点名称"格式
  438. expect(screen.getByText('广东省 广州市 市辖区 → 终点')).toBeInTheDocument()
  439. })
  440. test('返程路线应该优化终点显示为省市区格式', async () => {
  441. // Mock router with area names for return route
  442. const mockTaro = require('@tarojs/taro')
  443. mockTaro.useRouter.mockImplementation(() => ({
  444. params: {
  445. startAreaIds: JSON.stringify([1, 2]),
  446. endAreaIds: JSON.stringify([3, 4]),
  447. startAreaName: encodeURIComponent('广东省 广州市 市辖区'),
  448. date: '2025-10-31',
  449. vehicleType: 'bus',
  450. travelMode: 'carpool',
  451. activityId: '1',
  452. routeType: 'return'
  453. }
  454. }))
  455. // Mock API to return return route data
  456. const mockApi = require('../../src/api')
  457. mockApi.routeClient.search.$get.mockImplementation(() => Promise.resolve({
  458. status: 200,
  459. json: () => Promise.resolve({
  460. data: {
  461. routes: [
  462. {
  463. id: 1,
  464. name: '测试路线',
  465. description: null,
  466. startLocationId: 1,
  467. endLocationId: 2,
  468. startLocation: {
  469. id: 1,
  470. name: '起点',
  471. provinceId: 1,
  472. cityId: 1,
  473. districtId: 1,
  474. address: '起点地址'
  475. },
  476. endLocation: {
  477. id: 2,
  478. name: '终点',
  479. provinceId: 2,
  480. cityId: 2,
  481. districtId: 2,
  482. address: '终点地址'
  483. },
  484. pickupPoint: '上车点',
  485. dropoffPoint: '下车点',
  486. departureTime: '2025-10-31T08:00:00Z',
  487. vehicleType: 'bus',
  488. travelMode: 'carpool',
  489. price: 100,
  490. seatCount: 40,
  491. availableSeats: 20,
  492. activityId: 1,
  493. activity: {
  494. id: 1,
  495. name: '测试活动',
  496. description: null,
  497. venueLocationId: 3,
  498. venueLocation: {
  499. id: 3,
  500. name: '活动场地',
  501. provinceId: 3,
  502. cityId: 3,
  503. districtId: 3,
  504. address: '活动场地地址'
  505. },
  506. startDate: '2025-10-31',
  507. endDate: '2025-11-01'
  508. },
  509. routeType: 'return',
  510. isDisabled: 0,
  511. isDeleted: 0,
  512. createdAt: '2025-10-31T00:00:00Z',
  513. updatedAt: '2025-10-31T00:00:00Z'
  514. }
  515. ],
  516. activities: [
  517. {
  518. id: 1,
  519. name: '测试活动',
  520. description: null,
  521. venueLocationId: 3,
  522. venueLocation: {
  523. id: 3,
  524. name: '活动场地',
  525. provinceId: 3,
  526. cityId: 3,
  527. districtId: 3,
  528. address: '活动场地地址'
  529. },
  530. startDate: '2025-10-31',
  531. endDate: '2025-11-01'
  532. }
  533. ]
  534. }
  535. })
  536. }))
  537. render(
  538. <Wrapper>
  539. <ScheduleListPage />
  540. </Wrapper>
  541. )
  542. await waitFor(() => {
  543. expect(screen.getByText('选择班次')).toBeInTheDocument()
  544. })
  545. // 返程路线:应该显示"路线起点地点名称 → 搜索起点 省份 城市 区县"格式
  546. expect(screen.getByText('起点 → 广东省 广州市 市辖区')).toBeInTheDocument()
  547. })
  548. test('页面加载时不应该默认选择路线', async () => {
  549. // Mock router with no route data
  550. const mockTaro = require('@tarojs/taro')
  551. mockTaro.useRouter.mockImplementationOnce(() => ({
  552. params: {
  553. startAreaIds: JSON.stringify([1, 2]),
  554. endAreaIds: JSON.stringify([3, 4]),
  555. date: '2025-10-31',
  556. vehicleType: 'bus',
  557. travelMode: 'carpool',
  558. activityId: '1',
  559. routeType: 'departure'
  560. }
  561. }))
  562. const mockApi = require('../../src/api')
  563. mockApi.routeClient.search.$get.mockResolvedValueOnce({
  564. status: 200,
  565. json: () => Promise.resolve({
  566. data: {
  567. routes: [],
  568. activities: []
  569. }
  570. })
  571. })
  572. render(
  573. <Wrapper>
  574. <ScheduleListPage />
  575. </Wrapper>
  576. )
  577. await waitFor(() => {
  578. expect(screen.getByText('暂无班次')).toBeInTheDocument()
  579. })
  580. // 应该显示"请选择路线"而不是默认的路线信息
  581. expect(screen.getByText('请选择路线 → 请选择路线')).toBeInTheDocument()
  582. expect(screen.getByText('当前选择的路线暂无可用班次,请返回重新选择路线')).toBeInTheDocument()
  583. })
  584. test('应该正确处理省市区信息从路由参数传递', async () => {
  585. // Mock router with complete area information
  586. const mockTaro = require('@tarojs/taro')
  587. mockTaro.useRouter.mockImplementation(() => ({
  588. params: {
  589. startAreaIds: JSON.stringify([1, 2]),
  590. endAreaIds: JSON.stringify([3, 4]),
  591. startAreaName: encodeURIComponent('北京市 朝阳区'),
  592. endAreaName: encodeURIComponent('上海市 浦东新区'),
  593. date: '2025-10-31',
  594. vehicleType: 'bus',
  595. travelMode: 'carpool',
  596. activityId: '1',
  597. routeType: 'departure'
  598. }
  599. }))
  600. render(
  601. <Wrapper>
  602. <ScheduleListPage />
  603. </Wrapper>
  604. )
  605. await waitFor(() => {
  606. expect(screen.getByText('选择班次')).toBeInTheDocument()
  607. })
  608. // 应该正确显示从路由参数传递的省市区信息
  609. // 去程路线:显示搜索起点省市区到路线终点地点名称
  610. expect(screen.getByText('北京市 朝阳区 → 终点')).toBeInTheDocument()
  611. })
  612. })