/**
* 额度支付流程集成测试
* 测试完整额度支付流程
*/
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)
})
})