Bläddra i källkod

♻️ refactor(order): 优化订单页面导入结构

- 从@tarojs/taro直接导入showToast,移除utils/toast的依赖

✅ test(order): 完善订单页面测试用例

- 重构taroMock,添加ENV_TYPE常量和更多模拟函数
- 优化测试环境配置,添加QueryClientProvider包装
- 修复测试中Taro API调用的引用错误
- 增加测试覆盖率,验证更多边界情况
yourname 3 månader sedan
förälder
incheckning
e4cadecb78
3 ändrade filer med 203 tillägg och 89 borttagningar
  1. 1 2
      mini/src/pages/order/index.tsx
  2. 38 1
      mini/tests/__mocks__/taroMock.ts
  3. 164 86
      mini/tests/pages/order-page.test.tsx

+ 1 - 2
mini/src/pages/order/index.tsx

@@ -1,5 +1,5 @@
 import { View, Text, ScrollView } from '@tarojs/components'
-import { navigateBack, navigateTo, useRouter } from '@tarojs/taro'
+import { navigateBack, navigateTo, useRouter, showToast } from '@tarojs/taro'
 import { useState, useEffect } from 'react'
 import { useQuery, useMutation } from '@tanstack/react-query'
 import { format } from 'date-fns'
@@ -9,7 +9,6 @@ import { Navbar, NavbarPresets } from '@/components/ui/navbar'
 import { Button } from '@/components/ui/button'
 import { Card, CardContent } from '@/components/ui/card'
 import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
-import { showToast } from '@/utils/toast'
 import { getVehicleTypeText } from '@/types/route.types'
 import type { InferResponseType , InferRequestType} from 'hono/client'
 import {

+ 38 - 1
mini/tests/__mocks__/taroMock.ts

@@ -11,6 +11,22 @@ export const mockNavigateTo = jest.fn()
 export const mockShowModal = jest.fn()
 export const mockReLaunch = jest.fn()
 export const mockOpenCustomerServiceChat = jest.fn()
+export const mockUseRouter = jest.fn()
+export const mockRequestPayment = jest.fn()
+export const mockGetEnv = jest.fn()
+
+// 环境类型常量
+export const ENV_TYPE = {
+  WEAPP: 'WEAPP',
+  WEB: 'WEB',
+  RN: 'RN',
+  SWAN: 'SWAN',
+  ALIPAY: 'ALIPAY',
+  TT: 'TT',
+  QQ: 'QQ',
+  JD: 'JD',
+  HARMONY: 'HARMONY'
+}
 
 // 导出所有 mock 函数,便于在测试中访问
 export default {
@@ -22,10 +38,13 @@ export default {
 
   // 导航相关
   navigateTo: mockNavigateTo,
+  navigateBack: jest.fn(),
   reLaunch: mockReLaunch,
+  useRouter: () => mockUseRouter(),
 
   // 微信相关
   openCustomerServiceChat: mockOpenCustomerServiceChat,
+  requestPayment: mockRequestPayment,
 
   // 系统信息
   getSystemInfoSync: () => ({
@@ -38,5 +57,23 @@ export default {
     right: 314,
     bottom: 80,
     left: 227
-  })
+  }),
+  getEnv: mockGetEnv,
+
+  // 环境类型常量
+  ENV_TYPE
+}
+
+// 为命名导入导出所有函数
+export {
+  mockShowToast as showToast,
+  mockShowLoading as showLoading,
+  mockHideLoading as hideLoading,
+  mockShowModal as showModal,
+  mockNavigateTo as navigateTo,
+  mockReLaunch as reLaunch,
+  mockUseRouter as useRouter,
+  mockOpenCustomerServiceChat as openCustomerServiceChat,
+  mockRequestPayment as requestPayment,
+  mockGetEnv as getEnv
 }

+ 164 - 86
mini/tests/pages/order-page.test.tsx

@@ -2,72 +2,60 @@
  * 订单页面组件测试
  */
 
+import React from 'react'
 import { render, screen, fireEvent, waitFor } from '@testing-library/react'
 import '@testing-library/jest-dom'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
 import OrderPage from '@/pages/order/index'
 
-// Mock Taro相关API
-const mockNavigateTo = jest.fn()
-const mockUseRouter = jest.fn()
-
-// Mock 封装的 toast 函数
-let mockShowToast: jest.Mock
-
-jest.mock('@tarojs/taro', () => ({
-  navigateBack: jest.fn(),
-  useRouter: () => mockUseRouter(),
-  navigateTo: mockNavigateTo,
-  requestPayment: jest.fn(),
-  getSystemInfoSync: () => ({
-    statusBarHeight: 20
-  }),
-  getMenuButtonBoundingClientRect: () => ({
-    width: 87,
-    height: 32,
-    top: 48,
-    right: 314,
-    bottom: 80,
-    left: 227
-  })
-}))
+// 导入 Taro mock 函数
+import taroMock, { mockUseRouter } from '../../tests/__mocks__/taroMock'
 
-// Mock 封装的 toast 工具函数
-jest.mock('@/utils/toast', () => ({
-  showToast: jest.fn()
-}))
 
-beforeAll(() => {
-  mockShowToast = require('@/utils/toast').showToast
-})
 
 // Mock React Query
 const mockUseQuery = jest.fn()
 const mockUseMutation = jest.fn()
 
-jest.mock('@tanstack/react-query', () => ({
-  useQuery: (options: any) => mockUseQuery(options),
-  useMutation: (options: any) => mockUseMutation(options)
-}))
 
-// Mock cn工具函数
-jest.mock('@/utils/cn', () => ({
-  cn: (...inputs: any[]) => inputs.join(' ')
-}))
 
-// Mock platform工具
-jest.mock('@/utils/platform', () => ({
-  isWeapp: () => false
-}))
+// Mock React Query
+jest.mock('@tanstack/react-query', () => {
+  const actual = jest.requireActual('@tanstack/react-query')
+  return {
+    ...actual,
+    useQuery: (options: any) => mockUseQuery(options),
+    useMutation: (options: any) => mockUseMutation(options)
+  }
+})
+
+// 创建测试用的 QueryClient
+const createTestQueryClient = () => new QueryClient({
+  defaultOptions: {
+    queries: {
+      retry: false,
+    },
+  },
+})
 
-// Mock navbar组件
-// jest.mock('@/components/ui/navbar', () => ({
-//   Navbar: ({ children }: any) => <div data-testid="navbar">{children}</div>,
-//   NavbarPresets: {
-//     primary: {
-//       backgroundColor: 'bg-primary-600',
-//       textColor: 'text-white'
-//     }
-//   }
+// 包装组件
+const Wrapper = ({ children }: { children: React.ReactNode }) => {
+  const queryClient = createTestQueryClient()
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  )
+}
+
+// // Mock cn工具函数
+// jest.mock('@/utils/cn', () => ({
+//   cn: (...inputs: any[]) => inputs.join(' ')
+// }))
+
+// // Mock platform工具
+// jest.mock('@/utils/platform', () => ({
+//   isWeapp: () => false
 // }))
 
 // Mock Dialog组件
@@ -128,6 +116,8 @@ describe('OrderPage', () => {
   ]
 
   beforeEach(() => {
+    jest.clearAllMocks()
+
     mockUseRouter.mockReturnValue({
       params: {
         routeId: '1',
@@ -157,12 +147,22 @@ describe('OrderPage', () => {
       isPending: false
     }))
 
-    mockNavigateTo.mockClear()
-    mockShowToast.mockClear()
+    // 重置所有 Taro mock 调用记录
+    taroMock.showToast.mockClear()
+    taroMock.navigateTo.mockClear()
+    taroMock.requestPayment.mockClear()
+    taroMock.getEnv.mockClear()
+
+    // 设置默认环境为 WEB
+    taroMock.getEnv.mockReturnValue('WEB')
   })
 
   it('should render order page correctly', () => {
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     expect(screen.getByTestId('order-navbar')).toBeInTheDocument()
     expect(screen.getByTestId('activity-name')).toHaveTextContent('测试活动')
@@ -178,13 +178,21 @@ describe('OrderPage', () => {
       return { data: null, isLoading: false }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     expect(screen.getByText('加载中...')).toBeInTheDocument()
   })
 
   it('should handle phone number acquisition', async () => {
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     const getPhoneButton = screen.getByTestId('get-phone-button')
     expect(getPhoneButton).toBeInTheDocument()
@@ -196,7 +204,11 @@ describe('OrderPage', () => {
   it('should handle passenger selection', async () => {
     // 模拟已获取手机号的状态
     // 由于组件内部状态难以直接模拟,我们测试乘客选择器的基本功能
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 测试乘客选择器Dialog组件是否正常工作
     // 这里我们主要验证乘客选择器的渲染逻辑
@@ -211,7 +223,7 @@ describe('OrderPage', () => {
 
     // 由于未获取手机号,应该显示提示
     await waitFor(() => {
-      expect(mockShowToast).toHaveBeenCalledWith({
+      expect(taroMock.showToast).toHaveBeenCalledWith({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -220,14 +232,18 @@ describe('OrderPage', () => {
   })
 
   it('should validate payment prerequisites', async () => {
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示需要获取手机号的提示
     await waitFor(() => {
-      expect(mockShowToast).toHaveBeenCalledWith({
+      expect(taroMock.showToast).toHaveBeenCalledWith({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -264,10 +280,13 @@ describe('OrderPage', () => {
     }))
 
     // Mock成功的微信支付
-    const mockRequestPayment = require('@tarojs/taro').requestPayment
-    mockRequestPayment.mockResolvedValue({})
+    taroMock.requestPayment.mockResolvedValue({})
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 由于状态管理的复杂性,我们主要测试支付按钮的基本功能
     const payButton = screen.getByTestId('pay-button')
@@ -277,7 +296,7 @@ describe('OrderPage', () => {
     fireEvent.click(payButton)
 
     await waitFor(() => {
-      expect(mockShowToast).toHaveBeenCalledWith({
+      expect(taroMock.showToast).toHaveBeenCalledWith({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -294,14 +313,18 @@ describe('OrderPage', () => {
       isPending: false
     }))
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示需要获取手机号的提示(因为未获取手机号)
     await waitFor(() => {
-      expect(mockShowToast).toHaveBeenCalledWith({
+      expect(taroMock.showToast).toHaveBeenCalledWith({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -311,19 +334,22 @@ describe('OrderPage', () => {
 
   it('should handle user cancellation', async () => {
     // Mock用户取消支付
-    const mockRequestPayment = require('@tarojs/taro').requestPayment
-    mockRequestPayment.mockRejectedValue({
+    taroMock.requestPayment.mockRejectedValue({
       errMsg: 'requestPayment:fail cancel'
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示需要获取手机号的提示(因为未获取手机号)
     await waitFor(() => {
-      expect(mockShowToast).toHaveBeenCalledWith({
+      expect(taroMock.showToast).toHaveBeenCalledWith({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -332,7 +358,11 @@ describe('OrderPage', () => {
   })
 
   it('should calculate total price correctly', () => {
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 检查总价计算
     // 包车模式下应该显示固定价格
@@ -372,7 +402,11 @@ describe('OrderPage', () => {
       return { data: null, isLoading: false }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 验证拼车模式下的座位限制显示
     // 由于组件内部状态管理,我们主要验证基本功能
@@ -380,7 +414,11 @@ describe('OrderPage', () => {
   })
 
   it('should handle successful phone number acquisition', async () => {
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 由于组件内部状态管理,我们主要验证获取手机号按钮的存在
     const getPhoneButton = screen.getByTestId('get-phone-button')
@@ -391,7 +429,11 @@ describe('OrderPage', () => {
   })
 
   it('should handle phone number acquisition failure', async () => {
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 由于组件内部状态管理,我们主要验证获取手机号按钮的存在
     const getPhoneButton = screen.getByTestId('get-phone-button')
@@ -399,7 +441,11 @@ describe('OrderPage', () => {
   })
 
   it('should handle passenger deletion', async () => {
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 由于组件内部状态管理,我们主要验证删除按钮的存在
     // 这里需要模拟有乘客的情况,但由于状态是内部的,我们简化测试
@@ -420,7 +466,11 @@ describe('OrderPage', () => {
       return { data: null, isLoading: false }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 验证组件能够处理错误情况而不崩溃
     // 当路线数据加载失败时,组件应该显示加载状态
@@ -446,7 +496,11 @@ describe('OrderPage', () => {
       return { data: null, isLoading: false }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 验证组件能够处理错误情况而不崩溃
     expect(screen.getByTestId('order-navbar')).toBeInTheDocument()
@@ -462,14 +516,18 @@ describe('OrderPage', () => {
       isPending: false
     }))
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示需要获取手机号的提示(因为未获取手机号)
     await waitFor(() => {
-      expect(mockShowToast).toHaveBeenCalledWith({
+      expect(taroMock.showToast).toHaveBeenCalledWith({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -500,14 +558,18 @@ describe('OrderPage', () => {
       }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示需要获取手机号的提示(因为未获取手机号)
     await waitFor(() => {
-      expect(mockShowToast).toHaveBeenCalledWith({
+      expect(taroMock.showToast).toHaveBeenCalledWith({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -540,7 +602,11 @@ describe('OrderPage', () => {
       return { data: null, isLoading: false }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 验证拼车模式下的显示
     expect(screen.getByTestId('service-type')).toHaveTextContent('班次信息')
@@ -557,7 +623,11 @@ describe('OrderPage', () => {
       }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 验证包车模式下的显示
     expect(screen.getByTestId('service-type')).toHaveTextContent('包车服务')
@@ -574,7 +644,11 @@ describe('OrderPage', () => {
       }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 验证空活动名称时的默认显示
     expect(screen.getByTestId('activity-name')).toHaveTextContent('活动')
@@ -591,7 +665,11 @@ describe('OrderPage', () => {
       }
     })
 
-    render(<OrderPage />)
+    render(
+      <Wrapper>
+        <OrderPage />
+      </Wrapper>
+    )
 
     // 验证URL编码的活动名称被正确解码
     expect(screen.getByTestId('activity-name')).toHaveTextContent('测试活动名称')