Przeglądaj źródła

♻️ refactor(navbar): enhance component flexibility

- add [key: string]: any to NavbarProps for additional HTML attributes
- pass rest props to navbar container element

✅ test(order): improve test stability and coverage

- add data-testid attributes to key elements for reliable testing
- update test selectors to use data-testid instead of text content
- enhance Taro API mocks in setup.ts for more accurate testing environment

♻️ refactor(order): standardize Taro API usage

- replace direct showToast imports with Taro.showToast for consistency
- ensure all Taro API calls use the namespace import pattern
yourname 3 miesięcy temu
rodzic
commit
2cd7cfe708

+ 4 - 0
mini/src/components/ui/navbar.tsx

@@ -21,6 +21,8 @@ export interface NavbarProps {
   className?: string
   /** 是否在小程序环境下隐藏右侧按钮(默认false,会自动避让) */
   hideRightInWeapp?: boolean
+  /** 其他HTML属性 */
+  [key: string]: any
 }
 
 const systemInfo = Taro.getSystemInfoSync()
@@ -47,6 +49,7 @@ export const Navbar: React.FC<NavbarProps> = ({
   children,
   className,
   hideRightInWeapp,
+  ...rest
 }) => {
   // 处理左侧点击
   const handleLeftClick = () => {
@@ -171,6 +174,7 @@ export const Navbar: React.FC<NavbarProps> = ({
           className
         )}
         style={navbarStyle}
+        {...rest}
       >
         {/* 导航栏内容 */}
         <View

+ 21 - 17
mini/src/pages/order/index.tsx

@@ -1,5 +1,5 @@
 import { View, Text, ScrollView } from '@tarojs/components'
-import Taro, { useRouter, navigateTo, showToast } from '@tarojs/taro'
+import Taro, { useRouter, navigateTo } from '@tarojs/taro'
 import { useState, useEffect } from 'react'
 import { useQuery, useMutation } from '@tanstack/react-query'
 import { orderClient, paymentClient, routeClient, passengerClient } from '@/api'
@@ -139,14 +139,14 @@ export default function OrderPage() {
       // TODO: 实际项目中需要发送code到后端获取手机号
       setPhoneNumber('138****8888')
       setHasPhoneNumber(true)
-      showToast({
+      Taro.showToast({
         title: '手机号获取成功',
         icon: 'success',
         duration: 2000
       })
     } else {
       console.error('获取手机号失败:', e.detail.errMsg)
-      showToast({
+      Taro.showToast({
         title: '获取手机号失败,请重试',
         icon: 'error',
         duration: 2000
@@ -157,7 +157,7 @@ export default function OrderPage() {
   // 添加乘客
   const handleAddPassenger = () => {
     if (!hasPhoneNumber) {
-      showToast({
+      Taro.showToast({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -179,7 +179,7 @@ export default function OrderPage() {
     // 检查是否已经添加过这个乘车人
     const existingPassenger = passengers.find(p => p.idNumber === passenger.idNumber)
     if (existingPassenger) {
-      showToast({
+      Taro.showToast({
         title: '该乘车人已添加',
         icon: 'none',
         duration: 2000
@@ -327,7 +327,7 @@ export default function OrderPage() {
   // 创建订单并支付
   const handlePay = async () => {
     if (!hasPhoneNumber) {
-      showToast({
+      Taro.showToast({
         title: '请先获取手机号',
         icon: 'none',
         duration: 2000
@@ -336,7 +336,7 @@ export default function OrderPage() {
     }
 
     if (passengers.length === 0) {
-      showToast({
+      Taro.showToast({
         title: '请至少添加一个乘车人',
         icon: 'none',
         duration: 2000
@@ -345,7 +345,7 @@ export default function OrderPage() {
     }
 
     if (!schedule) {
-      showToast({
+      Taro.showToast({
         title: '路线数据加载中,请稍后',
         icon: 'none',
         duration: 2000
@@ -355,7 +355,7 @@ export default function OrderPage() {
     }
 
     if (!isCharter && passengers.length > (schedule.availableSeats || 0)) {
-      showToast({
+      Taro.showToast({
         title: `座位不足,最多可购买${schedule.availableSeats}张票`,
         icon: 'none',
         duration: 2000
@@ -396,7 +396,7 @@ export default function OrderPage() {
 
       if (wechatPaymentResult.success) {
         // 支付成功,跳转到支付成功页面
-        showToast({
+        Taro.showToast({
           title: '支付成功',
           icon: 'success',
           duration: 2000
@@ -408,13 +408,13 @@ export default function OrderPage() {
       } else {
         // 支付失败处理
         if (wechatPaymentResult.type === 'cancel') {
-          showToast({
+          Taro.showToast({
             title: '支付已取消',
             icon: 'none',
             duration: 2000
           })
         } else {
-          showToast({
+          Taro.showToast({
             title: wechatPaymentResult.message || '支付失败,请重试',
             icon: 'error',
             duration: 2000
@@ -427,7 +427,7 @@ export default function OrderPage() {
 
     } catch (error) {
       console.error('支付失败:', error)
-      showToast({
+      Taro.showToast({
         title: '支付失败,请重试',
         icon: 'error',
         duration: 2000
@@ -453,12 +453,13 @@ export default function OrderPage() {
         leftIcon="i-heroicons-arrow-left-20-solid"
         onClickLeft={() => Taro.navigateBack()}
         {...NavbarPresets.primary}
+        data-testid="order-navbar"
       />
 
       <ScrollView className="flex-1 pb-24">
         {/* 活动信息 */}
         <View className="bg-gradient-to-r from-primary to-primary-dark px-4 py-8 text-white shadow-lg">
-          <Text className="text-2xl font-bold text-center tracking-wide">{decodedActivityName || '活动'}</Text>
+          <Text className="text-2xl font-bold text-center tracking-wide" data-testid="activity-name">{decodedActivityName || '活动'}</Text>
         </View>
 
         {/* 班次信息 */}
@@ -466,7 +467,7 @@ export default function OrderPage() {
           <Card className={`${isCharter ? 'bg-gradient-to-br from-charter-dark to-charter-bg border-2 border-charter shadow-charter' : 'bg-white/95 shadow-lg backdrop-blur-md border border-white/40'} rounded-2xl`}>
             <CardContent className="p-6">
               <View className="flex justify-between items-center mb-6 pb-4 border-b border-gray-200">
-                <Text className={`text-xl font-bold tracking-wide ${isCharter ? 'text-charter' : 'text-gray-900'}`}>
+                <Text className={`text-xl font-bold tracking-wide ${isCharter ? 'text-charter' : 'text-gray-900'}`} data-testid="service-type">
                   {isCharter ? '包车服务' : '班次信息'}
                 </Text>
                 <View className="bg-primary text-white px-4 py-2 rounded-full text-xs font-semibold tracking-wide">
@@ -495,7 +496,7 @@ export default function OrderPage() {
                   <Text className={`text-sm font-medium ${isCharter ? 'text-gray-300' : 'text-gray-600'}`}>
                     {isCharter ? '包车价格' : '单人票价'}
                   </Text>
-                  <Text className={`text-base font-bold ${isCharter ? 'text-charter' : 'text-primary'}`}>
+                  <Text className={`text-base font-bold ${isCharter ? 'text-charter' : 'text-primary'}`} data-testid="price-per-unit">
                     ¥{schedule.price}{isCharter ? '/车' : '/人'}
                   </Text>
                 </View>
@@ -535,6 +536,7 @@ export default function OrderPage() {
                   openType="getPhoneNumber"
                   onGetPhoneNumber={handleGetPhoneNumber}
                   className="w-full"
+                  data-testid="get-phone-button"
                 >
                   <View className="flex items-center justify-center">
                     <View className="i-heroicons-phone-20-solid w-5 h-5 mr-2" />
@@ -595,6 +597,7 @@ export default function OrderPage() {
                 size="lg"
                 onClick={handleAddPassenger}
                 className="w-full"
+                data-testid="add-passenger-button"
               >
                 <View className="flex items-center justify-center">
                   <View className="i-heroicons-plus-20-solid w-5 h-5 mr-2" />
@@ -636,7 +639,7 @@ export default function OrderPage() {
 
               <View className="flex justify-between items-center pt-6 border-t border-gray-200">
                 <Text className={`text-xl font-bold tracking-wide ${isCharter ? 'text-charter' : 'text-gray-900'}`}>实付金额</Text>
-                <Text className={`text-3xl font-bold ${isCharter ? 'text-charter' : 'text-primary'}`}>¥{totalPrice}</Text>
+                <Text className={`text-3xl font-bold ${isCharter ? 'text-charter' : 'text-primary'}`} data-testid="total-price">¥{totalPrice}</Text>
               </View>
             </CardContent>
           </Card>
@@ -652,6 +655,7 @@ export default function OrderPage() {
             onClick={handlePay}
             disabled={createOrderMutation.isPending || createPaymentMutation.isPending}
             className="w-full shadow-lg"
+            data-testid="pay-button"
           >
             {createOrderMutation.isPending || createPaymentMutation.isPending ? (
               <View className="flex items-center justify-center">

+ 40 - 0
mini/tests/setup.ts

@@ -7,6 +7,46 @@ process.env.TARO_ENV = 'h5'
 process.env.TARO_PLATFORM = 'web'
 process.env.SUPPORT_TARO_POLYFILL = 'disabled'
 
+// Mock Taro 核心API
+const mockNavigateTo = jest.fn()
+const mockShowToast = jest.fn()
+const mockRequestPayment = jest.fn()
+const mockNavigateBack = jest.fn()
+
+jest.mock('@tarojs/taro', () => ({
+  __esModule: true,
+  default: {
+    getEnv: () => 'WEB',
+    ENV_TYPE: {
+      WEAPP: 'WEAPP',
+      WEB: 'WEB'
+    },
+    getSystemInfoSync: () => ({
+      statusBarHeight: 44,
+      screenWidth: 375,
+      screenHeight: 667,
+      windowWidth: 375,
+      windowHeight: 603,
+      pixelRatio: 2
+    }),
+    getMenuButtonBoundingClientRect: () => ({
+      width: 87,
+      height: 32,
+      top: 48,
+      right: 314,
+      bottom: 80,
+      left: 227
+    }),
+    navigateBack: mockNavigateBack,
+    requestPayment: mockRequestPayment
+  },
+  useRouter: () => ({
+    params: {}
+  }),
+  navigateTo: mockNavigateTo,
+  showToast: mockShowToast
+}))
+
 // Mock Taro 组件
 // eslint-disable-next-line react/display-name
 jest.mock('@tarojs/components', () => {

+ 45 - 12
mini/tests/unit/order-page.test.tsx

@@ -16,7 +16,19 @@ jest.mock('@tarojs/taro', () => ({
   useRouter: () => mockUseRouter(),
   navigateTo: mockNavigateTo,
   showToast: mockShowToast,
-  requestPayment: jest.fn()
+  requestPayment: jest.fn(),
+  getSystemInfoSync: () => ({
+    statusBarHeight: 20
+  }),
+  getMenuButtonBoundingClientRect: () => ({
+    width: 87,
+    height: 32,
+    top: 48,
+    right: 314,
+    bottom: 80,
+    left: 227
+  }),
+  navigateBack: jest.fn()
 }))
 
 // Mock React Query
@@ -28,6 +40,27 @@ jest.mock('@tanstack/react-query', () => ({
   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 navbar组件
+// jest.mock('@/components/ui/navbar', () => ({
+//   Navbar: ({ children }: any) => <div data-testid="navbar">{children}</div>,
+//   NavbarPresets: {
+//     primary: {
+//       backgroundColor: 'bg-primary-600',
+//       textColor: 'text-white'
+//     }
+//   }
+// }))
+
 // Mock API客户端
 jest.mock('@/api', () => ({
   orderClient: {
@@ -106,10 +139,10 @@ describe('OrderPage', () => {
   it('should render order page correctly', () => {
     render(<OrderPage />)
 
-    expect(screen.getByText('订单确认')).toBeInTheDocument()
-    expect(screen.getByText('测试活动')).toBeInTheDocument()
-    expect(screen.getByText('包车服务')).toBeInTheDocument()
-    expect(screen.getByText('¥100/车')).toBeInTheDocument()
+    expect(screen.getByTestId('order-navbar')).toBeInTheDocument()
+    expect(screen.getByTestId('activity-name')).toHaveTextContent('测试活动')
+    expect(screen.getByTestId('service-type')).toHaveTextContent('包车服务')
+    expect(screen.getByTestId('price-per-unit')).toHaveTextContent('¥100/车')
   })
 
   it('should show loading state', () => {
@@ -128,7 +161,7 @@ describe('OrderPage', () => {
   it('should handle phone number acquisition', async () => {
     render(<OrderPage />)
 
-    const getPhoneButton = screen.getByText('微信一键获取手机号')
+    const getPhoneButton = screen.getByTestId('get-phone-button')
     expect(getPhoneButton).toBeInTheDocument()
 
     // 这里可以模拟获取手机号的交互
@@ -138,7 +171,7 @@ describe('OrderPage', () => {
   it('should handle passenger selection', async () => {
     render(<OrderPage />)
 
-    const addPassengerButton = screen.getByText('添加乘车人')
+    const addPassengerButton = screen.getByTestId('add-passenger-button')
     fireEvent.click(addPassengerButton)
 
     // 应该显示乘客选择器
@@ -165,7 +198,7 @@ describe('OrderPage', () => {
 
     render(<OrderPage />)
 
-    const payButton = screen.getByText(/立即包车支付/)
+    const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示需要获取手机号的提示
@@ -217,7 +250,7 @@ describe('OrderPage', () => {
     // 这里需要模拟已获取手机号和添加乘客的状态
     // 由于状态管理的复杂性,这个测试可能需要更详细的设置
 
-    const payButton = screen.getByText(/立即包车支付/)
+    const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该调用订单创建和支付创建
@@ -244,7 +277,7 @@ describe('OrderPage', () => {
 
     render(<OrderPage />)
 
-    const payButton = screen.getByText(/立即包车支付/)
+    const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示支付失败的提示
@@ -266,7 +299,7 @@ describe('OrderPage', () => {
 
     render(<OrderPage />)
 
-    const payButton = screen.getByText(/立即包车支付/)
+    const payButton = screen.getByTestId('pay-button')
     fireEvent.click(payButton)
 
     // 应该显示支付已取消的提示
@@ -284,7 +317,7 @@ describe('OrderPage', () => {
 
     // 检查总价计算
     // 包车模式下应该显示固定价格
-    expect(screen.getByText('¥100')).toBeInTheDocument()
+    expect(screen.getByTestId('total-price')).toHaveTextContent('¥100')
   })
 
   it('should validate seat availability', async () => {