| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- /**
- * 额度支付流程集成测试
- * 测试完整额度支付流程
- */
- 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 }) => (
- <QueryClientProvider client={createTestQueryClient()}>
- {children}
- </QueryClientProvider>
- )
- // 测试数据工厂
- 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(
- <TestWrapper>
- <PaymentPage />
- </TestWrapper>
- )
- // 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(
- <TestWrapper>
- <PaymentPage />
- </TestWrapper>
- )
- // 等待页面加载
- 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(
- <TestWrapper>
- <PaymentPage />
- </TestWrapper>
- )
- // 等待页面加载
- 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(
- <TestWrapper>
- <PaymentPage />
- </TestWrapper>
- )
- // 等待页面加载
- 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(
- <TestWrapper>
- <PaymentPage />
- </TestWrapper>
- )
- // 等待页面加载
- 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(
- <TestWrapper>
- <PaymentPage />
- </TestWrapper>
- )
- // 等待页面加载(即使额度查询失败,页面也应该显示)
- 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(
- <TestWrapper>
- <PaymentPage />
- </TestWrapper>
- )
- // 等待页面加载
- 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)
- })
- })
|