/** * 支付页面额度支付单元测试 * 测试额度支付选项功能 */ import { render, screen, waitFor, fireEvent } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import PaymentPage from '@/pages/payment/index' import { creditBalanceClient } from '@/api' import { mockUseRouter, mockRedirectTo, mockShowToast } from '~/__mocks__/taroMock' // @tarojs/taro 已经在 jest.config.js 中通过 moduleNameMapper 重定向到 mock 文件 // 不需要额外 mock // Mock API客户端 jest.mock('@/api', () => ({ creditBalanceClient: { me: { $get: jest.fn(), }, payment: { $post: jest.fn(), }, }, paymentClient: { payment: { $post: jest.fn(), }, }, })) // Mock 支付工具函数 jest.mock('@/utils/payment', () => ({ requestWechatPayment: jest.fn(), PaymentStatus: { PENDING: 'pending', PROCESSING: 'processing', SUCCESS: 'success', FAILED: 'failed', }, PaymentStateManager: { getInstance: jest.fn(() => ({ setPaymentState: jest.fn(), clearPaymentState: jest.fn(), })), }, PaymentRateLimiter: { getInstance: jest.fn(() => ({ isRateLimited: jest.fn(() => ({ limited: false })), recordAttempt: jest.fn(), clearAttempts: jest.fn(), })), }, retryPayment: jest.fn(), })) // 创建测试QueryClient const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }) // 测试包装器 const TestWrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // 测试数据工厂 const createTestCreditBalance = (overrides = {}) => ({ totalLimit: 1000, usedAmount: 200, availableAmount: 800, isEnabled: true, ...overrides, }) const createTestPaymentData = () => ({ timeStamp: '1234567890', nonceStr: 'test-nonce', package: 'prepay_id=test_prepay_id', signType: 'MD5', paySign: 'test-sign', }) describe('支付页面额度支付功能测试', () => { beforeEach(() => { jest.clearAllMocks() jest.useFakeTimers() // 设置默认路由参数 mockUseRouter.mockReturnValue({ params: { orderId: '123', amount: '100', orderNo: 'ORD123456', }, }) }) afterEach(() => { jest.useRealTimers() }) test('应该正确渲染支付页面', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) render( ) // 验证页面标题 await waitFor(() => { expect(screen.getByTestId('payment-page-title')).toBeInTheDocument() }) // 验证订单信息显示 expect(screen.getByTestId('order-info')).toBeInTheDocument() expect(screen.getByTestId('order-no')).toHaveTextContent('ORD123456') expect(screen.getByTestId('payment-amount')).toHaveTextContent('¥100.00') // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 验证支付方式选项 expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument() expect(screen.getByTestId('credit-payment-option')).toBeInTheDocument() }) test('应该显示额度支付选项(只在额度满足时)', async () => { // Mock 额度查询返回正常数据(额度足够) const mockCreditBalance = createTestCreditBalance({ availableAmount: 800 }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 验证额度支付选项显示 const creditPaymentOption = screen.getByTestId('credit-payment-option') expect(creditPaymentOption).toBeInTheDocument() expect(creditPaymentOption).not.toHaveClass('opacity-50') }) test('额度为0时不应该显示额度支付选项', async () => { // Mock 额度查询返回额度为0的数据 const mockCreditBalance = createTestCreditBalance({ totalLimit: 0, usedAmount: 0, availableAmount: 0, isEnabled: false, }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) render( ) // 等待额度加载完成 - 现在额度未启用时,额度支付选项根本不显示 // 所以没有特定的文本需要等待 await waitFor(() => { // 验证只有微信支付选项显示 expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument() }) // 验证额度支付选项不显示 expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument() expect(screen.queryByTestId('credit-disabled-text')).not.toBeInTheDocument() }) test('额度不足时不应该显示额度支付选项', async () => { // Mock 额度查询返回额度不足的数据 const mockCreditBalance = createTestCreditBalance({ totalLimit: 50, usedAmount: 40, availableAmount: 10, // 可用额度10元,支付金额100元 isEnabled: true, }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) render( ) // 等待额度加载完成 await waitFor(() => { // 额度不足时,额度支付选项不应该显示 expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument() }) // 验证只有微信支付选项显示 expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument() }) test('应该可以切换支付方式', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 初始应该是微信支付选中 const wechatOption = screen.getByTestId('wechat-payment-option') expect(wechatOption).toHaveClass('border-blue-500') expect(screen.getByTestId('wechat-selected')).toBeInTheDocument() // 点击额度支付选项 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) // 验证额度支付被选中 await waitFor(() => { expect(creditOption).toHaveClass('border-blue-500') expect(screen.getByTestId('credit-selected')).toBeInTheDocument() }) // 验证支付按钮文字变为额度支付 expect(screen.getByTestId('pay-button')).toHaveTextContent('额度支付 ¥100.00') }) test('选择额度支付时应该显示额度详情(不显示可用额度)', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 点击额度支付选项 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) // 验证显示额度详情(不包含可用额度) await waitFor(() => { // 使用data-testid查询额度详情容器 const creditDetails = screen.getByTestId('credit-payment-details') expect(creditDetails).toBeInTheDocument() // 验证容器中包含额度信息(不包含可用额度) expect(creditDetails).toHaveTextContent(/使用信用额度支付,无需立即付款/) expect(creditDetails).toHaveTextContent(/总额度: ¥1000\.00/) expect(creditDetails).toHaveTextContent(/已用额度: ¥200\.00/) // 不应该包含可用额度 expect(creditDetails).not.toHaveTextContent(/可用额度:/) }) }) test('额度支付成功应该跳转到成功页面', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) // Mock 额度支付成功 const updatedBalance = createTestCreditBalance({ usedAmount: 300, availableAmount: 700 }) ;(creditBalanceClient.payment.$post as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(updatedBalance), }) render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 点击额度支付选项 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) // 点击支付按钮 const payButton = screen.getByText('额度支付 ¥100.00') fireEvent.click(payButton) // 验证调用了额度支付API await waitFor(() => { expect(creditBalanceClient.payment.$post).toHaveBeenCalledWith({ json: { referenceId: '123', // 现在传递订单ID而不是订单号 remark: '订单支付 - ORD123456', }, }) }) // 验证跳转到成功页面 // 推进时间以触发setTimeout中的跳转 jest.advanceTimersByTime(1600) await waitFor(() => { expect(mockRedirectTo).toHaveBeenCalledWith({ url: '/pages/payment-success/index?orderId=123&amount=100&paymentMethod=credit', }) }) }) test('额度支付失败应该显示错误信息', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) // Mock 额度支付失败 ;(creditBalanceClient.payment.$post as jest.Mock).mockResolvedValue({ status: 400, json: () => Promise.resolve({ message: '额度不足' }), }) render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 点击额度支付选项 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) // 点击支付按钮 const payButton = screen.getByText('额度支付 ¥100.00') fireEvent.click(payButton) // 验证显示错误信息 await waitFor(() => { expect(screen.getByText('额度不足')).toBeInTheDocument() }) }) test('应该与微信支付选项并行工作', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) // Mock 微信支付参数 const { paymentClient } = require('@/api') ;(paymentClient.payment.$post as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(createTestPaymentData()), }) render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 验证两个支付选项都存在 expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument() expect(screen.getByTestId('credit-payment-option')).toBeInTheDocument() // 默认选中微信支付 const wechatOption = screen.getByTestId('wechat-payment-option') expect(wechatOption).toHaveClass('border-blue-500') expect(screen.getByTestId('wechat-selected')).toBeInTheDocument() expect(screen.getByTestId('pay-button')).toHaveTextContent('微信支付 ¥100.00') // 可以切换到额度支付 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) await waitFor(() => { expect(creditOption).toHaveClass('border-blue-500') expect(screen.getByTestId('credit-selected')).toBeInTheDocument() expect(screen.getByTestId('pay-button')).toHaveTextContent('额度支付 ¥100.00') }) // 可以切换回微信支付 fireEvent.click(wechatOption) await waitFor(() => { expect(wechatOption).toHaveClass('border-blue-500') expect(screen.getByTestId('wechat-selected')).toBeInTheDocument() expect(screen.getByTestId('pay-button')).toHaveTextContent('微信支付 ¥100.00') }) }) test('页面加载时不应该自动调用微信支付API', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) // 获取paymentClient mock const { paymentClient } = require('@/api') render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 验证微信支付API没有被调用(页面加载时不应该自动调用) expect(paymentClient.payment.$post).not.toHaveBeenCalled() // 验证支付按钮可用 const payButton = screen.getByTestId('pay-button') expect(payButton).not.toBeDisabled() expect(payButton).toHaveTextContent('微信支付 ¥100.00') }) test('选择微信支付并点击支付按钮时才调用微信支付API', async () => { // Mock 额度查询返回正常数据 const mockCreditBalance = createTestCreditBalance() ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockCreditBalance), }) // Mock 微信支付参数 const { paymentClient } = require('@/api') ;(paymentClient.payment.$post as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(createTestPaymentData()), }) render( ) // 等待额度加载完成 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付') }) // 初始时微信支付API不应该被调用 expect(paymentClient.payment.$post).not.toHaveBeenCalled() // 点击支付按钮(默认选中微信支付) const payButton = screen.getByTestId('pay-button') fireEvent.click(payButton) // 验证微信支付API被调用 await waitFor(() => { expect(paymentClient.payment.$post).toHaveBeenCalledWith({ json: { orderId: 123, totalAmount: 10000, // 100元转换为分 description: '订单支付 - ORD123456', }, }) }) }) })