Explorar o código

✅ test(activity-select): add unit tests for ActivitySelectPage

- add data-testid attributes to activity items for testing
- create comprehensive test suite covering:
  - page rendering
  - activity list display
  - activity selection functionality
  - edge cases (no activities, loading state, API errors)
  - navigation and back button behavior
  - different route parameters handling
yourname hai 3 meses
pai
achega
d0521dd9ea

+ 2 - 0
mini/src/pages/select-activity/ActivitySelectPage.tsx

@@ -256,6 +256,7 @@ const ActivitySelectPage: React.FC = () => {
                       key={activity.id}
                       className="p-card border-b border-gray-100 last:border-b-0 active:bg-gray-50"
                       onClick={() => handleSelectActivity(activity, 'departure')}
+                      data-testid={`departure-activity-${activity.id}`}
                     >
                       <View className="flex">
                         {/* 活动图片占位 - 暂时移除以优化加载速度 */}
@@ -317,6 +318,7 @@ const ActivitySelectPage: React.FC = () => {
                       key={activity.id}
                       className="p-card border-b border-gray-100 last:border-b-0 active:bg-gray-50"
                       onClick={() => handleSelectActivity(activity, 'return')}
+                      data-testid={`return-activity-${activity.id}`}
                     >
                       <View className="flex">
                         {/* 活动图片占位 - 暂时移除以优化加载速度 */}

+ 458 - 0
mini/tests/pages/ActivitySelectPage.test.tsx

@@ -0,0 +1,458 @@
+import React from 'react'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import ActivitySelectPage from '../../src/pages/select-activity/ActivitySelectPage'
+
+// 导入 Taro mock 函数
+import taroMock, { mockUseRouter } from '../__mocks__/taroMock'
+
+// Mock API 客户端
+let mockRouteClient: any
+
+jest.mock('../../src/api', () => {
+  mockRouteClient = {
+    search: {
+      $get: jest.fn()
+    }
+  }
+
+  return {
+    routeClient: mockRouteClient
+  }
+})
+
+// Mock Navbar 组件
+jest.mock('@/components/ui/navbar', () => ({
+  Navbar: jest.fn(({ title, leftIcon, onClickLeft, ...props }) => (
+    <div data-testid="navbar">
+      <div data-testid="navbar-title">{title}</div>
+      <button data-testid="navbar-back" onClick={onClickLeft}>返回</button>
+    </div>
+  )),
+  NavbarPresets: {
+    primary: {}
+  }
+}))
+
+// 创建测试用的 QueryClient
+const createTestQueryClient = () => new QueryClient({
+  defaultOptions: {
+    queries: {
+      retry: false,
+    },
+  },
+})
+
+// 包装组件
+const Wrapper = ({ children }: { children: React.ReactNode }) => {
+  const queryClient = createTestQueryClient()
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  )
+}
+
+// 模拟活动数据
+const mockActivities = [
+  {
+    id: 1,
+    name: '音乐节活动',
+    description: '大型音乐节活动',
+    startDate: '2025-11-01T10:00:00Z',
+    endDate: '2025-11-01T22:00:00Z',
+    venueLocationId: 1,
+    venueLocation: {
+      id: 1,
+      name: '北京工人体育场',
+      provinceId: 1,
+      cityId: 11,
+      districtId: 101,
+      address: '北京市朝阳区工人体育场北路',
+      latitude: '39.929985',
+      longitude: '116.395645',
+      province: {
+        id: 1,
+        name: '北京市',
+        level: 1,
+        code: '110000'
+      },
+      city: {
+        id: 11,
+        name: '北京市',
+        level: 2,
+        code: '110100'
+      },
+      district: {
+        id: 101,
+        name: '朝阳区',
+        level: 3,
+        code: '110105'
+      }
+    }
+  },
+  {
+    id: 2,
+    name: '体育赛事',
+    description: '足球比赛',
+    startDate: '2025-11-02T15:00:00Z',
+    endDate: '2025-11-02T17:00:00Z',
+    venueLocationId: 2,
+    venueLocation: {
+      id: 2,
+      name: '上海体育场',
+      provinceId: 2,
+      cityId: 22,
+      districtId: 202,
+      address: '上海市徐汇区天钥桥路666号',
+      latitude: '31.230416',
+      longitude: '121.473701',
+      province: {
+        id: 2,
+        name: '上海市',
+        level: 1,
+        code: '310000'
+      },
+      city: {
+        id: 22,
+        name: '上海市',
+        level: 2,
+        code: '310100'
+      },
+      district: {
+        id: 202,
+        name: '徐汇区',
+        level: 3,
+        code: '310104'
+      }
+    }
+  }
+]
+
+// 模拟路线数据
+const mockRoutes = [
+  {
+    id: 1,
+    activityId: 1,
+    routeType: 'departure',
+    startLocation: {
+      id: 1,
+      name: '北京南站',
+      provinceId: 1,
+      cityId: 11,
+      districtId: 101,
+      province: { id: 1, name: '北京市' },
+      city: { id: 11, name: '北京市' },
+      district: { id: 101, name: '朝阳区' }
+    },
+    endLocation: {
+      id: 2,
+      name: '上海虹桥站',
+      provinceId: 2,
+      cityId: 22,
+      districtId: 202,
+      province: { id: 2, name: '上海市' },
+      city: { id: 22, name: '上海市' },
+      district: { id: 202, name: '徐汇区' }
+    }
+  },
+  {
+    id: 2,
+    activityId: 2,
+    routeType: 'return',
+    startLocation: {
+      id: 2,
+      name: '上海虹桥站',
+      provinceId: 2,
+      cityId: 22,
+      districtId: 202,
+      province: { id: 2, name: '上海市' },
+      city: { id: 22, name: '上海市' },
+      district: { id: 202, name: '徐汇区' }
+    },
+    endLocation: {
+      id: 1,
+      name: '北京南站',
+      provinceId: 1,
+      cityId: 11,
+      districtId: 101,
+      province: { id: 1, name: '北京市' },
+      city: { id: 11, name: '北京市' },
+      district: { id: 101, name: '朝阳区' }
+    }
+  }
+]
+
+describe('活动选择页面测试', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+
+    // 设置默认的路由参数
+    mockUseRouter.mockReturnValue({
+      params: {
+        startAreaIds: JSON.stringify([1, 11, 101]),
+        endAreaIds: JSON.stringify([2, 22, 202]),
+        startAreaName: encodeURIComponent('北京市 北京市 朝阳区'),
+        endAreaName: encodeURIComponent('上海市 上海市 徐汇区'),
+        date: '2025-11-01',
+        vehicleType: 'bus',
+        travelMode: 'carpool'
+      }
+    })
+
+    // 设置默认的API响应
+    mockRouteClient.search.$get.mockResolvedValue({
+      status: 200,
+      json: async () => ({
+        success: true,
+        data: {
+          routes: mockRoutes,
+          activities: mockActivities
+        }
+      })
+    })
+  })
+
+  test('应该正确渲染活动选择页面', async () => {
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 检查导航栏
+    await waitFor(() => {
+      expect(screen.getByTestId('navbar')).toBeInTheDocument()
+      expect(screen.getByTestId('navbar-title')).toHaveTextContent('选择活动')
+    })
+
+    // 检查头部信息
+    expect(screen.getByText('北京市 北京市 朝阳区 → 上海市 上海市 徐汇区')).toBeInTheDocument()
+    expect(screen.getByText('2025-11-01')).toBeInTheDocument()
+
+    // 检查页面标题
+    expect(screen.getByText('选择观看活动')).toBeInTheDocument()
+    expect(screen.getByText('系统已根据您选择的地区自动匹配相关活动')).toBeInTheDocument()
+
+    // 检查去程活动区域
+    expect(screen.getByText('去程活动')).toBeInTheDocument()
+    expect(screen.getByText('前往上海市 上海市 徐汇区观看活动')).toBeInTheDocument()
+
+    // 检查返程活动区域
+    expect(screen.getByText('返程活动')).toBeInTheDocument()
+    expect(screen.getByText('从北京市 北京市 朝阳区观看活动后返回')).toBeInTheDocument()
+  })
+
+  test('应该正确显示活动列表', async () => {
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('音乐节活动')).toBeInTheDocument()
+      expect(screen.getByText('体育赛事')).toBeInTheDocument()
+    })
+
+    // 检查活动信息完整显示
+    expect(screen.getByText('朝阳区 · 北京市 · 北京市')).toBeInTheDocument()
+    expect(screen.getByText('北京市朝阳区工人体育场北路')).toBeInTheDocument()
+    expect(screen.getByText('到达:上海市 上海市 徐汇区')).toBeInTheDocument()
+
+    // 检查活动图片占位符已被注释掉,不显示
+    expect(screen.queryByText('活动图片')).not.toBeInTheDocument()
+  })
+
+  test('应该正确处理活动选择', async () => {
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('音乐节活动')).toBeInTheDocument()
+    })
+
+    // 点击去程活动
+    const activityItem = screen.getByTestId('departure-activity-1')
+    fireEvent.click(activityItem)
+
+    // 检查导航被调用
+    expect(taroMock.navigateTo).toHaveBeenCalledWith({
+      url: expect.stringContaining('pages/schedule-list/ScheduleListPage')
+    })
+
+    // 检查导航参数正确
+    const navigateCall = taroMock.navigateTo.mock.calls[0][0]
+    expect(navigateCall.url).toContain('startAreaIds=[1,11,101]')
+    expect(navigateCall.url).toContain('endAreaIds=[2,22,202]')
+    expect(navigateCall.url).toContain('date=2025-11-01')
+    expect(navigateCall.url).toContain('vehicleType=bus')
+    expect(navigateCall.url).toContain('travelMode=carpool')
+    expect(navigateCall.url).toContain('activityId=1')
+    expect(navigateCall.url).toContain('routeType=departure')
+  })
+
+  test('应该处理返程活动选择', async () => {
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('体育赛事')).toBeInTheDocument()
+    })
+
+    // 点击返程活动
+    const returnActivity = screen.getByTestId('return-activity-2')
+    fireEvent.click(returnActivity)
+
+    // 检查导航被调用
+    expect(taroMock.navigateTo).toHaveBeenCalledWith({
+      url: expect.stringContaining('pages/schedule-list/ScheduleListPage')
+    })
+
+    // 检查导航参数正确
+    const navigateCall = taroMock.navigateTo.mock.calls[0][0]
+    expect(navigateCall.url).toContain('routeType=return')
+    expect(navigateCall.url).toContain('activityId=2')
+  })
+
+  test('应该处理无活动的情况', async () => {
+    // 模拟无活动数据
+    mockRouteClient.search.$get.mockResolvedValue({
+      status: 200,
+      json: async () => ({
+        success: true,
+        data: {
+          routes: [],
+          activities: []
+        }
+      })
+    })
+
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 检查无活动状态显示
+    await waitFor(() => {
+      expect(screen.getByText('暂无去程活动')).toBeInTheDocument()
+      expect(screen.getByText('上海市 上海市 徐汇区当前没有相关活动')).toBeInTheDocument()
+
+      expect(screen.getByText('暂无返程活动')).toBeInTheDocument()
+      expect(screen.getByText('北京市 北京市 朝阳区当前没有相关活动')).toBeInTheDocument()
+    })
+  })
+
+  test('应该处理加载状态', () => {
+    // 模拟API延迟
+    mockRouteClient.search.$get.mockImplementation(() => new Promise(() => {}))
+
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 检查加载状态显示
+    expect(screen.getByText('正在加载活动')).toBeInTheDocument()
+    expect(screen.getByText('请稍候...')).toBeInTheDocument()
+  })
+
+  test('应该处理API错误', async () => {
+    // 模拟API错误
+    mockRouteClient.search.$get.mockRejectedValue(new Error('网络错误'))
+
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 检查无活动状态显示(因为API错误导致无数据)
+    await waitFor(() => {
+      expect(screen.getByText('暂无去程活动')).toBeInTheDocument()
+      expect(screen.getByText('暂无返程活动')).toBeInTheDocument()
+    })
+  })
+
+  test('应该验证布局优化效果', async () => {
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('音乐节活动')).toBeInTheDocument()
+    })
+
+    // 验证图片占位符已被注释掉,不显示
+    expect(screen.queryByText('活动图片')).not.toBeInTheDocument()
+
+    // 验证活动信息完整显示
+    expect(screen.getByText('音乐节活动')).toBeInTheDocument()
+    expect(screen.getByText('2025-11-01')).toBeInTheDocument()
+    expect(screen.getByText('朝阳区 · 北京市 · 北京市')).toBeInTheDocument()
+    expect(screen.getByText('北京市朝阳区工人体育场北路')).toBeInTheDocument()
+    expect(screen.getByText('到达:上海市 上海市 徐汇区')).toBeInTheDocument()
+  })
+
+  test('应该处理返回按钮', async () => {
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('navbar')).toBeInTheDocument()
+    })
+
+    // 点击返回按钮
+    const backButton = screen.getByTestId('navbar-back')
+    fireEvent.click(backButton)
+
+    // 检查返回导航被调用
+    expect(taroMock.navigateBack).toHaveBeenCalled()
+  })
+
+  test('应该处理不同的路由参数', async () => {
+    // 设置不同的路由参数
+    mockUseRouter.mockReturnValue({
+      params: {
+        startAreaIds: '[3,33,303]',
+        endAreaIds: '[4,44,404]',
+        startAreaName: encodeURIComponent('广州市 广东省 天河区'),
+        endAreaName: encodeURIComponent('深圳市 广东省 福田区'),
+        date: '2025-11-02',
+        vehicleType: 'business',
+        travelMode: 'charter'
+      }
+    })
+
+    render(
+      <Wrapper>
+        <ActivitySelectPage />
+      </Wrapper>
+    )
+
+    // 检查头部信息显示正确的地区
+    await waitFor(() => {
+      expect(screen.getByText('广州市 广东省 天河区 → 深圳市 广东省 福田区')).toBeInTheDocument()
+      expect(screen.getByText('2025-11-02')).toBeInTheDocument()
+    })
+  })
+})