/** * 额度支付流程集成测试 * 测试完整额度支付流程 */ import { render, screen, waitFor, fireEvent } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import PaymentPage from '@/pages/payment/index' import { creditBalanceClient, paymentClient } from '@/api' import { mockUseRouter, mockNavigateTo, mockShowToast, mockRedirectTo } 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 initialBalance = createTestCreditBalance({ availableAmount: 800 }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(initialBalance), }) // Mock 额度支付成功 const updatedBalance = createTestCreditBalance({ usedAmount: 300, // 原200 + 支付100 = 300 availableAmount: 700 // 原800 - 支付100 = 700 }) ;(creditBalanceClient.payment.$post as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(updatedBalance), }) render( ) // 1. 验证页面加载和额度显示 await waitFor(() => { expect(screen.getByTestId('payment-page-title')).toBeInTheDocument() expect(screen.getByTestId('available-amount-text')).toHaveTextContent(/使用信用额度支付/) }) // 2. 选择额度支付方式 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) await waitFor(() => { expect(creditOption).toHaveClass('border-blue-500') expect(screen.getByTestId('pay-button')).toHaveTextContent('额度支付 ¥100.00') }) // 3. 验证额度详情显示 const creditDetails = screen.getByTestId('credit-payment-details') expect(creditDetails).toHaveTextContent(/使用信用额度支付,无需立即付款/) // 不应该包含额度详情 expect(creditDetails).not.toHaveTextContent(/可用额度:/) expect(creditDetails).not.toHaveTextContent(/总额度:/) expect(creditDetails).not.toHaveTextContent(/已用额度:/) // 4. 点击支付按钮 const payButton = screen.getByTestId('pay-button') fireEvent.click(payButton) // 5. 验证支付处理中状态 await waitFor(() => { expect(screen.getByText('支付中...')).toBeInTheDocument() }) // 6. 验证调用了额度支付API await waitFor(() => { expect(creditBalanceClient.payment.$post).toHaveBeenCalledWith({ json: { referenceId: '123', // 传递订单ID而不是订单号 remark: '订单支付 - ORD123456', }, }) }) // 7. 验证支付成功状态 await waitFor(() => { // 使用类名选择器找到支付成功状态文本 expect(screen.getByText('支付成功', { selector: 'span.text-xl' })).toBeInTheDocument() }) // 8. 推进时间以触发跳转 jest.advanceTimersByTime(1600) // 9. 验证跳转到成功页面 await waitFor(() => { expect(mockRedirectTo).toHaveBeenCalledWith({ url: '/pages/payment-success/index?orderId=123&amount=100&paymentMethod=credit', }) }) }) test('额度支付失败流程:显示错误并可以重试', async () => { // Mock 额度查询返回正常数据 const initialBalance = createTestCreditBalance({ availableAmount: 800 }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(initialBalance), }) // Mock 额度支付第一次失败,第二次成功 let paymentCallCount = 0 ;(creditBalanceClient.payment.$post as jest.Mock).mockImplementation(() => { paymentCallCount++ if (paymentCallCount === 1) { return Promise.resolve({ status: 400, json: () => Promise.resolve({ message: '额度支付失败,请重试' }), }) } else { return Promise.resolve({ status: 200, json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 300, availableAmount: 700 })), }) } }) render( ) // 等待页面加载 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent(/使用信用额度支付/) }) // 选择额度支付 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) // 点击支付按钮(第一次失败) const payButton = screen.getByTestId('pay-button') fireEvent.click(payButton) // 验证显示错误信息 await waitFor(() => { expect(screen.getByText('额度支付失败,请重试')).toBeInTheDocument() expect(screen.getByText('重试支付')).toBeInTheDocument() }) // 点击重试按钮 const retryButton = screen.getByText('重试支付') fireEvent.click(retryButton) // 验证第二次支付成功 await waitFor(() => { // 使用更精确的选择器,避免多个"支付成功"元素 expect(screen.getByText('支付成功', { selector: 'span.text-xl' })).toBeInTheDocument() }) // 推进时间以触发跳转 jest.advanceTimersByTime(1600) await waitFor(() => { expect(mockRedirectTo).toHaveBeenCalledWith({ url: '/pages/payment-success/index?orderId=123&amount=100&paymentMethod=credit', }) }) }) test('额度不足时的支付流程', async () => { // Mock 额度查询返回额度不足的数据 const initialBalance = createTestCreditBalance({ totalLimit: 50, usedAmount: 45, availableAmount: 5 // 可用额度5元,支付金额100元 }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(initialBalance), }) render( ) // 等待页面加载 await waitFor(() => { // 额度不足时,额度支付选项不应该显示 expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument() }) // 验证支付按钮没有被禁用(因为默认选择微信支付) const payButton = screen.getByTestId('pay-button') expect(payButton).not.toBeDisabled() }) test('额度为0时的支付流程', async () => { // Mock 额度查询返回额度为0的数据 const initialBalance = createTestCreditBalance({ totalLimit: 0, usedAmount: 0, availableAmount: 0, isEnabled: false, }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(initialBalance), }) render( ) // 等待页面加载 await waitFor(() => { expect(screen.getByTestId('payment-page-title')).toBeInTheDocument() }) // 验证额度支付选项不显示(因为额度为0且未启用) expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument() // 验证支付按钮没有被禁用(因为默认选择微信支付) const payButton = screen.getByTestId('pay-button') expect(payButton).not.toBeDisabled() }) test('额度支付与微信支付切换流程', async () => { // Mock 额度查询返回正常数据 const initialBalance = createTestCreditBalance({ availableAmount: 800 }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(initialBalance), }) // Mock 微信支付参数 const mockPaymentData = createTestPaymentData() ;(paymentClient.payment.$post as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(mockPaymentData), }) 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.getByText('微信支付 ¥100.00')).toBeInTheDocument() // 切换到额度支付 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) await waitFor(() => { expect(creditOption).toHaveClass('border-blue-500') expect(screen.getByTestId('pay-button')).toHaveTextContent('额度支付 ¥100.00') }) // 验证额度详情显示 expect(screen.getByText('• 使用信用额度支付,无需立即付款')).toBeInTheDocument() // 切换回微信支付 fireEvent.click(wechatOption) await waitFor(() => { expect(wechatOption).toHaveClass('border-blue-500') expect(screen.getByTestId('pay-button')).toHaveTextContent('微信支付 ¥100.00') }) // 验证额度详情隐藏 expect(screen.queryByText('• 使用信用额度支付,无需立即付款')).not.toBeInTheDocument() }) test('网络异常时的降级处理', async () => { // Mock 额度查询网络异常 ;(creditBalanceClient.me.$get as jest.Mock).mockRejectedValue(new Error('网络连接失败')) render( ) // 等待页面加载(即使额度查询失败,页面也应该显示) await waitFor(() => { // 使用data-testid查询支付页面标题 expect(screen.getByTestId('payment-page-title')).toBeInTheDocument() }) // 验证额度支付选项不显示(因为查询失败) // 当额度查询失败时,额度支付选项不应该显示 expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument() // 验证支付按钮没有被禁用(因为默认选择微信支付,额度查询失败不影响微信支付) const payButton = screen.getByTestId('pay-button') expect(payButton).not.toBeDisabled() }) test('支付过程中的取消操作', async () => { // Mock 额度查询返回正常数据 const initialBalance = createTestCreditBalance({ availableAmount: 800 }) ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({ status: 200, json: () => Promise.resolve(initialBalance), }) // Mock 额度支付延迟(模拟用户取消) let resolvePayment: any const paymentPromise = new Promise((resolve) => { resolvePayment = resolve }) ;(creditBalanceClient.payment.$post as jest.Mock).mockReturnValue(paymentPromise) render( ) // 等待页面加载 await waitFor(() => { expect(screen.getByTestId('available-amount-text')).toHaveTextContent(/使用信用额度支付/) }) // 选择额度支付 const creditOption = screen.getByTestId('credit-payment-option') fireEvent.click(creditOption) // 点击支付按钮 const payButton = screen.getByTestId('pay-button') fireEvent.click(payButton) // 验证支付处理中状态 await waitFor(() => { expect(screen.getByText('支付中...')).toBeInTheDocument() }) // 此时页面应该显示支付处理中,用户无法进行其他操作 await waitFor(() => { // 重新获取按钮元素(文本已变为"支付处理中...") const processingButton = screen.getByText('支付处理中...') // 检查按钮是否被禁用 expect(processingButton).toBeDisabled() }) // 模拟支付完成(超时或其他原因) resolvePayment({ status: 200, json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 300, availableAmount: 700 })), }) // 验证支付成功 await waitFor(() => { // 使用更具体的查询,避免多个"支付成功"元素 expect(screen.getByText('支付成功', { selector: 'span.text-xl' })).toBeInTheDocument() }) // 推进时间以触发跳转 jest.advanceTimersByTime(1600) }) })