|
|
@@ -0,0 +1,489 @@
|
|
|
+/**
|
|
|
+ * 额度恢复集成测试
|
|
|
+ * 测试额度恢复逻辑
|
|
|
+ */
|
|
|
+
|
|
|
+import { render, screen, waitFor, fireEvent } from '@testing-library/react'
|
|
|
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
+import { creditBalanceClient } from '@/api'
|
|
|
+import { mockShowToast, mockShowModal } from '~/__mocks__/taroMock'
|
|
|
+
|
|
|
+// 由于额度恢复主要在服务端处理,这里测试前端相关的恢复逻辑
|
|
|
+// 包括额度查询、状态显示等
|
|
|
+
|
|
|
+// @tarojs/taro 已经在 jest.config.js 中通过 moduleNameMapper 重定向到 mock 文件
|
|
|
+// 不需要额外 mock
|
|
|
+
|
|
|
+// Mock API客户端
|
|
|
+jest.mock('@/api', () => ({
|
|
|
+ creditBalanceClient: {
|
|
|
+ me: {
|
|
|
+ $get: jest.fn(),
|
|
|
+ },
|
|
|
+ checkout: {
|
|
|
+ $post: 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 TestCreditBalanceComponent = () => {
|
|
|
+ const { data: creditBalance, isLoading, error, refetch } = useQuery({
|
|
|
+ queryKey: ['credit-balance-test'],
|
|
|
+ queryFn: async () => {
|
|
|
+ const response = await creditBalanceClient.me.$get({})
|
|
|
+ if (response.status === 200) {
|
|
|
+ return response.json()
|
|
|
+ }
|
|
|
+ throw new Error('查询失败')
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const handleCheckout = async () => {
|
|
|
+ try {
|
|
|
+ const response = await creditBalanceClient.checkout.$post({
|
|
|
+ json: {
|
|
|
+ amount: 100,
|
|
|
+ referenceId: 'ORD123456',
|
|
|
+ remark: '结账恢复额度',
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.status === 200) {
|
|
|
+ mockShowToast({ title: '额度恢复成功', icon: 'success' })
|
|
|
+ refetch()
|
|
|
+ } else {
|
|
|
+ mockShowToast({ title: '额度恢复失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ mockShowToast({ title: '额度恢复异常', icon: 'none' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return <div data-testid="loading">加载中...</div>
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error) {
|
|
|
+ return (
|
|
|
+ <div data-testid="error">
|
|
|
+ <div>加载失败</div>
|
|
|
+ <button data-testid="retry-button" onClick={() => refetch()}>重试</button>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div data-testid="credit-balance-component">
|
|
|
+ <div data-testid="used-amount">已用额度: ¥{creditBalance?.usedAmount.toFixed(2)}</div>
|
|
|
+ <div data-testid="available-amount">可用额度: ¥{creditBalance?.availableAmount.toFixed(2)}</div>
|
|
|
+ <button data-testid="checkout-button" onClick={handleCheckout}>结账恢复额度</button>
|
|
|
+ <button data-testid="refresh-button" onClick={() => refetch()}>刷新额度</button>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+// Mock useQuery
|
|
|
+import { useQuery } from '@tanstack/react-query'
|
|
|
+jest.mock('@tanstack/react-query', () => ({
|
|
|
+ ...jest.requireActual('@tanstack/react-query'),
|
|
|
+ useQuery: jest.fn(),
|
|
|
+}))
|
|
|
+
|
|
|
+describe('额度恢复集成测试', () => {
|
|
|
+ beforeEach(() => {
|
|
|
+ jest.clearAllMocks()
|
|
|
+ })
|
|
|
+
|
|
|
+ test('额度查询和显示功能', async () => {
|
|
|
+ // Mock 额度查询返回正常数据
|
|
|
+ const mockCreditBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
|
|
|
+ ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(mockCreditBalance),
|
|
|
+ })
|
|
|
+
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: mockCreditBalance,
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: jest.fn(),
|
|
|
+ })
|
|
|
+
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 验证额度信息显示
|
|
|
+ expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥200.00')
|
|
|
+ expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥800.00')
|
|
|
+ expect(screen.getByTestId('checkout-button')).toBeInTheDocument()
|
|
|
+ expect(screen.getByTestId('refresh-button')).toBeInTheDocument()
|
|
|
+ })
|
|
|
+
|
|
|
+ test('结账恢复额度操作', async () => {
|
|
|
+ // Mock 额度查询返回正常数据
|
|
|
+ const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
|
|
|
+ ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(initialBalance),
|
|
|
+ })
|
|
|
+
|
|
|
+ // Mock 结账恢复成功
|
|
|
+ const updatedBalance = createTestCreditBalance({ usedAmount: 100, availableAmount: 900 })
|
|
|
+ ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(updatedBalance),
|
|
|
+ })
|
|
|
+
|
|
|
+ const mockRefetch = jest.fn()
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: initialBalance,
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 点击结账恢复按钮
|
|
|
+ const checkoutButton = screen.getByTestId('checkout-button')
|
|
|
+ fireEvent.click(checkoutButton)
|
|
|
+
|
|
|
+ // 验证调用了结账恢复API
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(creditBalanceClient.checkout.$post).toHaveBeenCalledWith({
|
|
|
+ json: {
|
|
|
+ amount: 100,
|
|
|
+ referenceId: 'ORD123456',
|
|
|
+ remark: '结账恢复额度',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ // 验证显示成功提示
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
+ title: '额度恢复成功',
|
|
|
+ icon: 'success',
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ // 验证重新查询额度
|
|
|
+ expect(mockRefetch).toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ test('结账恢复额度失败处理', async () => {
|
|
|
+ // Mock 额度查询返回正常数据
|
|
|
+ const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
|
|
|
+ ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(initialBalance),
|
|
|
+ })
|
|
|
+
|
|
|
+ // Mock 结账恢复失败
|
|
|
+ ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue({
|
|
|
+ status: 400,
|
|
|
+ json: () => Promise.resolve({ message: '恢复失败' }),
|
|
|
+ })
|
|
|
+
|
|
|
+ const mockRefetch = jest.fn()
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: initialBalance,
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 点击结账恢复按钮
|
|
|
+ const checkoutButton = screen.getByTestId('checkout-button')
|
|
|
+ fireEvent.click(checkoutButton)
|
|
|
+
|
|
|
+ // 验证显示失败提示
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(mockShowToast).toHaveBeenCalledWith({
|
|
|
+ title: '额度恢复失败',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ // 验证没有重新查询额度
|
|
|
+ expect(mockRefetch).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ test('额度查询失败后的重试功能', async () => {
|
|
|
+ // Mock 额度查询第一次失败,第二次成功
|
|
|
+ let queryCallCount = 0
|
|
|
+ ;(creditBalanceClient.me.$get as jest.Mock).mockImplementation(() => {
|
|
|
+ queryCallCount++
|
|
|
+ if (queryCallCount === 1) {
|
|
|
+ return Promise.reject(new Error('网络错误'))
|
|
|
+ } else {
|
|
|
+ return Promise.resolve({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 200 })),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const mockRefetch = jest.fn(() => {
|
|
|
+ // 模拟重试逻辑
|
|
|
+ return Promise.resolve()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 第一次渲染:错误状态
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: null,
|
|
|
+ isLoading: false,
|
|
|
+ error: new Error('网络错误'),
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ const { rerender } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 验证错误状态显示
|
|
|
+ expect(screen.getByTestId('error')).toBeInTheDocument()
|
|
|
+ expect(screen.getByTestId('retry-button')).toBeInTheDocument()
|
|
|
+
|
|
|
+ // 点击重试按钮
|
|
|
+ const retryButton = screen.getByTestId('retry-button')
|
|
|
+ fireEvent.click(retryButton)
|
|
|
+
|
|
|
+ // 验证调用了重试函数
|
|
|
+ expect(mockRefetch).toHaveBeenCalled()
|
|
|
+
|
|
|
+ // 模拟重试成功后的状态
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: createTestCreditBalance({ usedAmount: 200, availableAmount: 800 }),
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ // 重新渲染
|
|
|
+ rerender(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 验证正常显示额度信息
|
|
|
+ expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥200.00')
|
|
|
+ expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥800.00')
|
|
|
+ })
|
|
|
+
|
|
|
+ test('额度恢复的幂等性验证(前端角度)', async () => {
|
|
|
+ // Mock 额度查询返回正常数据
|
|
|
+ const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
|
|
|
+ ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(initialBalance),
|
|
|
+ })
|
|
|
+
|
|
|
+ // Mock 结账恢复API,多次调用返回相同结果
|
|
|
+ const mockCheckoutResponse = {
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 100, availableAmount: 900 })),
|
|
|
+ }
|
|
|
+ ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue(mockCheckoutResponse)
|
|
|
+
|
|
|
+ const mockRefetch = jest.fn()
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: initialBalance,
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 连续点击多次结账恢复按钮
|
|
|
+ const checkoutButton = screen.getByTestId('checkout-button')
|
|
|
+ fireEvent.click(checkoutButton)
|
|
|
+ fireEvent.click(checkoutButton)
|
|
|
+ fireEvent.click(checkoutButton)
|
|
|
+
|
|
|
+ // 验证API被调用了3次
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(creditBalanceClient.checkout.$post).toHaveBeenCalledTimes(3)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 验证每次调用参数相同
|
|
|
+ expect(creditBalanceClient.checkout.$post).toHaveBeenNthCalledWith(1, {
|
|
|
+ json: {
|
|
|
+ amount: 100,
|
|
|
+ referenceId: 'ORD123456',
|
|
|
+ remark: '结账恢复额度',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(creditBalanceClient.checkout.$post).toHaveBeenNthCalledWith(2, {
|
|
|
+ json: {
|
|
|
+ amount: 100,
|
|
|
+ referenceId: 'ORD123456',
|
|
|
+ remark: '结账恢复额度',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(creditBalanceClient.checkout.$post).toHaveBeenNthCalledWith(3, {
|
|
|
+ json: {
|
|
|
+ amount: 100,
|
|
|
+ referenceId: 'ORD123456',
|
|
|
+ remark: '结账恢复额度',
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ // 验证重新查询额度被调用了3次
|
|
|
+ expect(mockRefetch).toHaveBeenCalledTimes(3)
|
|
|
+ })
|
|
|
+
|
|
|
+ test('额度数据刷新功能', async () => {
|
|
|
+ // Mock 额度查询
|
|
|
+ const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
|
|
|
+ ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(initialBalance),
|
|
|
+ })
|
|
|
+
|
|
|
+ const mockRefetch = jest.fn()
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: initialBalance,
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 点击刷新按钮
|
|
|
+ const refreshButton = screen.getByTestId('refresh-button')
|
|
|
+ fireEvent.click(refreshButton)
|
|
|
+
|
|
|
+ // 验证调用了刷新函数
|
|
|
+ expect(mockRefetch).toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ test('额度查询加载状态', () => {
|
|
|
+ // Mock 加载状态
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: null,
|
|
|
+ isLoading: true,
|
|
|
+ error: null,
|
|
|
+ refetch: jest.fn(),
|
|
|
+ })
|
|
|
+
|
|
|
+ render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 验证加载状态显示
|
|
|
+ expect(screen.getByTestId('loading')).toHaveTextContent('加载中...')
|
|
|
+ })
|
|
|
+
|
|
|
+ test('额度恢复后的状态更新', async () => {
|
|
|
+ // 模拟额度恢复前后的状态变化
|
|
|
+ const beforeRestore = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
|
|
|
+ const afterRestore = createTestCreditBalance({ usedAmount: 100, availableAmount: 900 })
|
|
|
+
|
|
|
+ let currentData = beforeRestore
|
|
|
+ const mockRefetch = jest.fn(() => {
|
|
|
+ currentData = afterRestore
|
|
|
+ return Promise.resolve()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 第一次渲染:恢复前状态
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: currentData,
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ const { rerender } = render(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 验证恢复前状态
|
|
|
+ expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥200.00')
|
|
|
+ expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥800.00')
|
|
|
+
|
|
|
+ // Mock 结账恢复API
|
|
|
+ ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve(afterRestore),
|
|
|
+ })
|
|
|
+
|
|
|
+ // 点击结账恢复按钮
|
|
|
+ const checkoutButton = screen.getByTestId('checkout-button')
|
|
|
+ fireEvent.click(checkoutButton)
|
|
|
+
|
|
|
+ // 模拟恢复后的查询状态
|
|
|
+ ;(useQuery as jest.Mock).mockReturnValue({
|
|
|
+ data: afterRestore,
|
|
|
+ isLoading: false,
|
|
|
+ error: null,
|
|
|
+ refetch: mockRefetch,
|
|
|
+ })
|
|
|
+
|
|
|
+ // 重新渲染
|
|
|
+ rerender(
|
|
|
+ <TestWrapper>
|
|
|
+ <TestCreditBalanceComponent />
|
|
|
+ </TestWrapper>
|
|
|
+ )
|
|
|
+
|
|
|
+ // 验证恢复后状态
|
|
|
+ expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥100.00')
|
|
|
+ expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥900.00')
|
|
|
+ })
|
|
|
+})
|