credit-payment.test.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /**
  2. * 支付页面额度支付单元测试
  3. * 测试额度支付选项功能
  4. */
  5. import { render, screen, waitFor, fireEvent } from '@testing-library/react'
  6. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
  7. import PaymentPage from '@/pages/payment/index'
  8. import { creditBalanceClient } from '@/api'
  9. import { mockUseRouter, mockRedirectTo, mockShowToast } from '~/__mocks__/taroMock'
  10. // @tarojs/taro 已经在 jest.config.js 中通过 moduleNameMapper 重定向到 mock 文件
  11. // 不需要额外 mock
  12. // Mock API客户端
  13. jest.mock('@/api', () => ({
  14. creditBalanceClient: {
  15. me: {
  16. $get: jest.fn(),
  17. },
  18. payment: {
  19. $post: jest.fn(),
  20. },
  21. },
  22. paymentClient: {
  23. payment: {
  24. $post: jest.fn(),
  25. },
  26. },
  27. }))
  28. // Mock 支付工具函数
  29. jest.mock('@/utils/payment', () => ({
  30. requestWechatPayment: jest.fn(),
  31. PaymentStatus: {
  32. PENDING: 'pending',
  33. PROCESSING: 'processing',
  34. SUCCESS: 'success',
  35. FAILED: 'failed',
  36. },
  37. PaymentStateManager: {
  38. getInstance: jest.fn(() => ({
  39. setPaymentState: jest.fn(),
  40. clearPaymentState: jest.fn(),
  41. })),
  42. },
  43. PaymentRateLimiter: {
  44. getInstance: jest.fn(() => ({
  45. isRateLimited: jest.fn(() => ({ limited: false })),
  46. recordAttempt: jest.fn(),
  47. clearAttempts: jest.fn(),
  48. })),
  49. },
  50. retryPayment: jest.fn(),
  51. }))
  52. // 创建测试QueryClient
  53. const createTestQueryClient = () => new QueryClient({
  54. defaultOptions: {
  55. queries: { retry: false },
  56. mutations: { retry: false },
  57. },
  58. })
  59. // 测试包装器
  60. const TestWrapper = ({ children }: { children: React.ReactNode }) => (
  61. <QueryClientProvider client={createTestQueryClient()}>
  62. {children}
  63. </QueryClientProvider>
  64. )
  65. // 测试数据工厂
  66. const createTestCreditBalance = (overrides = {}) => ({
  67. totalLimit: 1000,
  68. usedAmount: 200,
  69. availableAmount: 800,
  70. isEnabled: true,
  71. ...overrides,
  72. })
  73. const createTestPaymentData = () => ({
  74. timeStamp: '1234567890',
  75. nonceStr: 'test-nonce',
  76. package: 'prepay_id=test_prepay_id',
  77. signType: 'MD5',
  78. paySign: 'test-sign',
  79. })
  80. describe('支付页面额度支付功能测试', () => {
  81. beforeEach(() => {
  82. jest.clearAllMocks()
  83. jest.useFakeTimers()
  84. // 设置默认路由参数
  85. mockUseRouter.mockReturnValue({
  86. params: {
  87. orderId: '123',
  88. amount: '100',
  89. orderNo: 'ORD123456',
  90. },
  91. })
  92. })
  93. afterEach(() => {
  94. jest.useRealTimers()
  95. })
  96. test('应该正确渲染支付页面', async () => {
  97. // Mock 额度查询返回正常数据
  98. const mockCreditBalance = createTestCreditBalance()
  99. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  100. status: 200,
  101. json: () => Promise.resolve(mockCreditBalance),
  102. })
  103. render(
  104. <TestWrapper>
  105. <PaymentPage />
  106. </TestWrapper>
  107. )
  108. // 验证页面标题
  109. await waitFor(() => {
  110. expect(screen.getByTestId('payment-page-title')).toBeInTheDocument()
  111. })
  112. // 验证订单信息显示
  113. expect(screen.getByTestId('order-info')).toBeInTheDocument()
  114. expect(screen.getByTestId('order-no')).toHaveTextContent('ORD123456')
  115. expect(screen.getByTestId('payment-amount')).toHaveTextContent('¥100.00')
  116. // 等待额度加载完成
  117. await waitFor(() => {
  118. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  119. })
  120. // 验证支付方式选项
  121. expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument()
  122. expect(screen.getByTestId('credit-payment-option')).toBeInTheDocument()
  123. })
  124. test('应该显示额度支付选项(只在额度满足时)', async () => {
  125. // Mock 额度查询返回正常数据(额度足够)
  126. const mockCreditBalance = createTestCreditBalance({ availableAmount: 800 })
  127. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  128. status: 200,
  129. json: () => Promise.resolve(mockCreditBalance),
  130. })
  131. render(
  132. <TestWrapper>
  133. <PaymentPage />
  134. </TestWrapper>
  135. )
  136. // 等待额度加载完成
  137. await waitFor(() => {
  138. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  139. })
  140. // 验证额度支付选项显示
  141. const creditPaymentOption = screen.getByTestId('credit-payment-option')
  142. expect(creditPaymentOption).toBeInTheDocument()
  143. expect(creditPaymentOption).not.toHaveClass('opacity-50')
  144. })
  145. test('额度为0时不应该显示额度支付选项', async () => {
  146. // Mock 额度查询返回额度为0的数据
  147. const mockCreditBalance = createTestCreditBalance({
  148. totalLimit: 0,
  149. usedAmount: 0,
  150. availableAmount: 0,
  151. isEnabled: false,
  152. })
  153. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  154. status: 200,
  155. json: () => Promise.resolve(mockCreditBalance),
  156. })
  157. render(
  158. <TestWrapper>
  159. <PaymentPage />
  160. </TestWrapper>
  161. )
  162. // 等待额度加载完成 - 现在额度未启用时,额度支付选项根本不显示
  163. // 所以没有特定的文本需要等待
  164. await waitFor(() => {
  165. // 验证只有微信支付选项显示
  166. expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument()
  167. })
  168. // 验证额度支付选项不显示
  169. expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument()
  170. expect(screen.queryByTestId('credit-disabled-text')).not.toBeInTheDocument()
  171. })
  172. test('额度不足时不应该显示额度支付选项', async () => {
  173. // Mock 额度查询返回额度不足的数据
  174. const mockCreditBalance = createTestCreditBalance({
  175. totalLimit: 50,
  176. usedAmount: 40,
  177. availableAmount: 10, // 可用额度10元,支付金额100元
  178. isEnabled: true,
  179. })
  180. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  181. status: 200,
  182. json: () => Promise.resolve(mockCreditBalance),
  183. })
  184. render(
  185. <TestWrapper>
  186. <PaymentPage />
  187. </TestWrapper>
  188. )
  189. // 等待额度加载完成
  190. await waitFor(() => {
  191. // 额度不足时,额度支付选项不应该显示
  192. expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument()
  193. })
  194. // 验证只有微信支付选项显示
  195. expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument()
  196. })
  197. test('应该可以切换支付方式', async () => {
  198. // Mock 额度查询返回正常数据
  199. const mockCreditBalance = createTestCreditBalance()
  200. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  201. status: 200,
  202. json: () => Promise.resolve(mockCreditBalance),
  203. })
  204. render(
  205. <TestWrapper>
  206. <PaymentPage />
  207. </TestWrapper>
  208. )
  209. // 等待额度加载完成
  210. await waitFor(() => {
  211. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  212. })
  213. // 初始应该是微信支付选中
  214. const wechatOption = screen.getByTestId('wechat-payment-option')
  215. expect(wechatOption).toHaveClass('border-blue-500')
  216. expect(screen.getByTestId('wechat-selected')).toBeInTheDocument()
  217. // 点击额度支付选项
  218. const creditOption = screen.getByTestId('credit-payment-option')
  219. fireEvent.click(creditOption)
  220. // 验证额度支付被选中
  221. await waitFor(() => {
  222. expect(creditOption).toHaveClass('border-blue-500')
  223. expect(screen.getByTestId('credit-selected')).toBeInTheDocument()
  224. })
  225. // 验证支付按钮文字变为额度支付
  226. expect(screen.getByTestId('pay-button')).toHaveTextContent('额度支付 ¥100.00')
  227. })
  228. test('选择额度支付时应该显示额度详情(不显示可用额度)', async () => {
  229. // Mock 额度查询返回正常数据
  230. const mockCreditBalance = createTestCreditBalance()
  231. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  232. status: 200,
  233. json: () => Promise.resolve(mockCreditBalance),
  234. })
  235. render(
  236. <TestWrapper>
  237. <PaymentPage />
  238. </TestWrapper>
  239. )
  240. // 等待额度加载完成
  241. await waitFor(() => {
  242. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  243. })
  244. // 点击额度支付选项
  245. const creditOption = screen.getByTestId('credit-payment-option')
  246. fireEvent.click(creditOption)
  247. // 验证显示额度详情(不包含可用额度)
  248. await waitFor(() => {
  249. // 使用data-testid查询额度详情容器
  250. const creditDetails = screen.getByTestId('credit-payment-details')
  251. expect(creditDetails).toBeInTheDocument()
  252. // 验证容器中包含额度信息(不包含可用额度)
  253. expect(creditDetails).toHaveTextContent(/使用信用额度支付,无需立即付款/)
  254. expect(creditDetails).toHaveTextContent(/总额度: ¥1000\.00/)
  255. expect(creditDetails).toHaveTextContent(/已用额度: ¥200\.00/)
  256. // 不应该包含可用额度
  257. expect(creditDetails).not.toHaveTextContent(/可用额度:/)
  258. })
  259. })
  260. test('额度支付成功应该跳转到成功页面', async () => {
  261. // Mock 额度查询返回正常数据
  262. const mockCreditBalance = createTestCreditBalance()
  263. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  264. status: 200,
  265. json: () => Promise.resolve(mockCreditBalance),
  266. })
  267. // Mock 额度支付成功
  268. const updatedBalance = createTestCreditBalance({ usedAmount: 300, availableAmount: 700 })
  269. ;(creditBalanceClient.payment.$post as jest.Mock).mockResolvedValue({
  270. status: 200,
  271. json: () => Promise.resolve(updatedBalance),
  272. })
  273. render(
  274. <TestWrapper>
  275. <PaymentPage />
  276. </TestWrapper>
  277. )
  278. // 等待额度加载完成
  279. await waitFor(() => {
  280. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  281. })
  282. // 点击额度支付选项
  283. const creditOption = screen.getByTestId('credit-payment-option')
  284. fireEvent.click(creditOption)
  285. // 点击支付按钮
  286. const payButton = screen.getByText('额度支付 ¥100.00')
  287. fireEvent.click(payButton)
  288. // 验证调用了额度支付API
  289. await waitFor(() => {
  290. expect(creditBalanceClient.payment.$post).toHaveBeenCalledWith({
  291. json: {
  292. referenceId: '123', // 现在传递订单ID而不是订单号
  293. remark: '订单支付 - ORD123456',
  294. },
  295. })
  296. })
  297. // 验证跳转到成功页面
  298. // 推进时间以触发setTimeout中的跳转
  299. jest.advanceTimersByTime(1600)
  300. await waitFor(() => {
  301. expect(mockRedirectTo).toHaveBeenCalledWith({
  302. url: '/pages/payment-success/index?orderId=123&amount=100&paymentMethod=credit',
  303. })
  304. })
  305. })
  306. test('额度支付失败应该显示错误信息', async () => {
  307. // Mock 额度查询返回正常数据
  308. const mockCreditBalance = createTestCreditBalance()
  309. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  310. status: 200,
  311. json: () => Promise.resolve(mockCreditBalance),
  312. })
  313. // Mock 额度支付失败
  314. ;(creditBalanceClient.payment.$post as jest.Mock).mockResolvedValue({
  315. status: 400,
  316. json: () => Promise.resolve({ message: '额度不足' }),
  317. })
  318. render(
  319. <TestWrapper>
  320. <PaymentPage />
  321. </TestWrapper>
  322. )
  323. // 等待额度加载完成
  324. await waitFor(() => {
  325. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  326. })
  327. // 点击额度支付选项
  328. const creditOption = screen.getByTestId('credit-payment-option')
  329. fireEvent.click(creditOption)
  330. // 点击支付按钮
  331. const payButton = screen.getByText('额度支付 ¥100.00')
  332. fireEvent.click(payButton)
  333. // 验证显示错误信息
  334. await waitFor(() => {
  335. expect(screen.getByText('额度不足')).toBeInTheDocument()
  336. })
  337. })
  338. test('应该与微信支付选项并行工作', async () => {
  339. // Mock 额度查询返回正常数据
  340. const mockCreditBalance = createTestCreditBalance()
  341. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  342. status: 200,
  343. json: () => Promise.resolve(mockCreditBalance),
  344. })
  345. // Mock 微信支付参数
  346. const { paymentClient } = require('@/api')
  347. ;(paymentClient.payment.$post as jest.Mock).mockResolvedValue({
  348. status: 200,
  349. json: () => Promise.resolve(createTestPaymentData()),
  350. })
  351. render(
  352. <TestWrapper>
  353. <PaymentPage />
  354. </TestWrapper>
  355. )
  356. // 等待额度加载完成
  357. await waitFor(() => {
  358. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  359. })
  360. // 验证两个支付选项都存在
  361. expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument()
  362. expect(screen.getByTestId('credit-payment-option')).toBeInTheDocument()
  363. // 默认选中微信支付
  364. const wechatOption = screen.getByTestId('wechat-payment-option')
  365. expect(wechatOption).toHaveClass('border-blue-500')
  366. expect(screen.getByTestId('wechat-selected')).toBeInTheDocument()
  367. expect(screen.getByTestId('pay-button')).toHaveTextContent('微信支付 ¥100.00')
  368. // 可以切换到额度支付
  369. const creditOption = screen.getByTestId('credit-payment-option')
  370. fireEvent.click(creditOption)
  371. await waitFor(() => {
  372. expect(creditOption).toHaveClass('border-blue-500')
  373. expect(screen.getByTestId('credit-selected')).toBeInTheDocument()
  374. expect(screen.getByTestId('pay-button')).toHaveTextContent('额度支付 ¥100.00')
  375. })
  376. // 可以切换回微信支付
  377. fireEvent.click(wechatOption)
  378. await waitFor(() => {
  379. expect(wechatOption).toHaveClass('border-blue-500')
  380. expect(screen.getByTestId('wechat-selected')).toBeInTheDocument()
  381. expect(screen.getByTestId('pay-button')).toHaveTextContent('微信支付 ¥100.00')
  382. })
  383. })
  384. test('页面加载时不应该自动调用微信支付API', async () => {
  385. // Mock 额度查询返回正常数据
  386. const mockCreditBalance = createTestCreditBalance()
  387. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  388. status: 200,
  389. json: () => Promise.resolve(mockCreditBalance),
  390. })
  391. // 获取paymentClient mock
  392. const { paymentClient } = require('@/api')
  393. render(
  394. <TestWrapper>
  395. <PaymentPage />
  396. </TestWrapper>
  397. )
  398. // 等待额度加载完成
  399. await waitFor(() => {
  400. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  401. })
  402. // 验证微信支付API没有被调用(页面加载时不应该自动调用)
  403. expect(paymentClient.payment.$post).not.toHaveBeenCalled()
  404. // 验证支付按钮可用
  405. const payButton = screen.getByTestId('pay-button')
  406. expect(payButton).not.toBeDisabled()
  407. expect(payButton).toHaveTextContent('微信支付 ¥100.00')
  408. })
  409. test('选择微信支付并点击支付按钮时才调用微信支付API', async () => {
  410. // Mock 额度查询返回正常数据
  411. const mockCreditBalance = createTestCreditBalance()
  412. ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
  413. status: 200,
  414. json: () => Promise.resolve(mockCreditBalance),
  415. })
  416. // Mock 微信支付参数
  417. const { paymentClient } = require('@/api')
  418. ;(paymentClient.payment.$post as jest.Mock).mockResolvedValue({
  419. status: 200,
  420. json: () => Promise.resolve(createTestPaymentData()),
  421. })
  422. render(
  423. <TestWrapper>
  424. <PaymentPage />
  425. </TestWrapper>
  426. )
  427. // 等待额度加载完成
  428. await waitFor(() => {
  429. expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
  430. })
  431. // 初始时微信支付API不应该被调用
  432. expect(paymentClient.payment.$post).not.toHaveBeenCalled()
  433. // 点击支付按钮(默认选中微信支付)
  434. const payButton = screen.getByTestId('pay-button')
  435. fireEvent.click(payButton)
  436. // 验证微信支付API被调用
  437. await waitFor(() => {
  438. expect(paymentClient.payment.$post).toHaveBeenCalledWith({
  439. json: {
  440. orderId: 123,
  441. totalAmount: 10000, // 100元转换为分
  442. description: '订单支付 - ORD123456',
  443. },
  444. })
  445. })
  446. })
  447. })