Procházet zdrojové kódy

✅ test(combination-query): 完成组合查询测试套件

- 【后端API集成测试】创建完整的用户端路线搜索API组合查询集成测试,覆盖所有组合查询场景
- 【前端组件测试】创建首页组合查询逻辑详细测试,验证参数映射和样式切换
- 【E2E流程测试】创建完整的端到端组合查询流程测试,验证完整用户流程
- 【文档更新】更新故事文档,标记测试任务为已完成并添加测试文件清单
yourname před 3 měsíci
rodič
revize
1b3d0e3fe1

+ 12 - 4
docs/stories/005.004.story.md

@@ -57,10 +57,10 @@ Implemented
 - [x] 验证前端页面正确显示组合查询选项 (AC: 5)
   - [x] 测试首页出行方式选择器显示
   - [x] 验证组合查询参数正确传递
-- [ ] 编写测试,验证组合查询逻辑正确性 (AC: 3, 7, 8, 9)
-  - [ ] 编写后端API组合查询测试
-  - [ ] 编写前端组件组合查询测试
-  - [ ] 编写E2E测试验证完整组合查询流程
+- [x] 编写测试,验证组合查询逻辑正确性 (AC: 3, 7, 8, 9)
+  - [x] 编写后端API组合查询测试
+  - [x] 编写前端组件组合查询测试
+  - [x] 编写E2E测试验证完整组合查询流程
 
 ## Dev Notes
 
@@ -221,6 +221,9 @@ GET /api/v1/routes/search?vehicleType=bus,business&travelMode=charter
 6. **小程序页面更新**: 首页、活动选择、班次列表页面全部支持组合查询
 7. **组合查询映射**: 正确实现了大巴拼车、商务车、包车三种查询方式
 8. **种子数据更新**: 为所有路线添加travelMode字段,覆盖所有组合查询场景
+9. **后端API组合查询测试**: 创建了完整的用户端路线搜索API集成测试
+10. **前端组件组合查询测试**: 创建了首页组合查询逻辑的详细测试
+11. **E2E组合查询流程测试**: 创建了完整的端到端组合查询流程测试
 
 ### File List
 **后端文件**:
@@ -236,6 +239,11 @@ GET /api/v1/routes/search?vehicleType=bus,business&travelMode=charter
 - `mini/src/pages/select-activity/ActivitySelectPage.tsx` - 支持travelMode参数
 - `mini/src/pages/schedule-list/ScheduleListPage.tsx` - 完整组合查询支持
 
+**测试文件**:
+- `tests/integration/server/routes/search.integration.test.ts` - 后端API组合查询集成测试
+- `mini/tests/pages/HomePageCombinationQuery.test.tsx` - 前端组合查询逻辑测试
+- `tests/e2e/specs/travel-flow/combination-query.spec.ts` - E2E组合查询流程测试
+
 **种子数据文件**:
 - `scripts/seed.ts` - 为所有路线添加travelMode字段,覆盖组合查询场景
 

+ 454 - 0
mini/tests/pages/HomePageCombinationQuery.test.tsx

@@ -0,0 +1,454 @@
+import React from 'react'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import HomePage from '../../src/pages/home/index'
+
+// Mock Taro 导航
+const mockNavigateTo = jest.fn()
+jest.mock('@tarojs/taro', () => ({
+  navigateTo: mockNavigateTo
+}))
+
+// Mock AreaPicker 组件
+jest.mock('../../src/components/AreaPicker', () => ({
+  AreaPicker: jest.fn(({ visible, onClose, onConfirm, value, title }) => {
+    if (!visible) return null
+
+    return (
+      <div data-testid="area-picker">
+        <div data-testid="area-picker-title">{title}</div>
+        <button data-testid="area-picker-confirm" onClick={() => onConfirm([1, 11, 101], [
+          { id: 1, name: '北京市', type: 'province' },
+          { id: 11, name: '北京市', type: 'city' },
+          { id: 101, name: '朝阳区', type: 'district' }
+        ])}>确认</button>
+        <button data-testid="area-picker-cancel" onClick={onClose}>取消</button>
+      </div>
+    )
+  })
+}))
+
+// Mock TabBarLayout 组件
+jest.mock('@/layouts/tab-bar-layout', () => ({
+  TabBarLayout: jest.fn(({ children, activeKey, className }) => (
+    <div data-testid="tab-bar-layout" data-active-key={activeKey} className={className}>
+      {children}
+    </div>
+  ))
+}))
+
+// 创建测试用的 QueryClient
+const createTestQueryClient = () => new QueryClient({
+  defaultOptions: {
+    queries: {
+      retry: false,
+    },
+  },
+})
+
+// 包装组件
+const Wrapper = ({ children }: { children: React.ReactNode }) => {
+  const queryClient = createTestQueryClient()
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  )
+}
+
+describe('首页组合查询逻辑测试', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+  })
+
+  describe('组合查询映射逻辑', () => {
+    test('应该正确映射大巴拼车组合查询参数', async () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 设置出发地和目的地
+      const startLocationButton = screen.getByText('出发地').closest('button')
+      fireEvent.click(startLocationButton!)
+      const confirmButton = screen.getByTestId('area-picker-confirm')
+      fireEvent.click(confirmButton)
+
+      const endLocationButton = screen.getByText('目的地').closest('button')
+      fireEvent.click(endLocationButton!)
+      fireEvent.click(confirmButton)
+
+      // 等待状态更新
+      await waitFor(() => {
+        expect(screen.getAllByText('北京市 北京市 朝阳区')).toHaveLength(2)
+      })
+
+      // 默认选择大巴拼车,点击查询
+      const searchButton = screen.getByText('查询路线')
+      fireEvent.click(searchButton)
+
+      // 验证导航参数
+      expect(mockNavigateTo).toHaveBeenCalledWith({
+        url: expect.stringContaining('vehicleType=bus')
+      })
+      expect(mockNavigateTo).toHaveBeenCalledWith({
+        url: expect.stringContaining('travelMode=carpool')
+      })
+    })
+
+    test('应该正确映射商务车组合查询参数', async () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 设置出发地和目的地
+      const startLocationButton = screen.getByText('出发地').closest('button')
+      fireEvent.click(startLocationButton!)
+      const confirmButton = screen.getByTestId('area-picker-confirm')
+      fireEvent.click(confirmButton)
+
+      const endLocationButton = screen.getByText('目的地').closest('button')
+      fireEvent.click(endLocationButton!)
+      fireEvent.click(confirmButton)
+
+      // 等待状态更新
+      await waitFor(() => {
+        expect(screen.getAllByText('北京市 北京市 朝阳区')).toHaveLength(2)
+      })
+
+      // 选择商务车
+      const businessOption = screen.getByText('商务车')
+      fireEvent.click(businessOption)
+
+      // 点击查询
+      const searchButton = screen.getByText('查询路线')
+      fireEvent.click(searchButton)
+
+      // 验证导航参数 - 商务车支持拼车和包车两种出行方式
+      expect(mockNavigateTo).toHaveBeenCalledWith({
+        url: expect.stringContaining('vehicleType=business')
+      })
+      expect(mockNavigateTo).toHaveBeenCalledWith({
+        url: expect.stringContaining('travelMode=carpool,charter')
+      })
+    })
+
+    test('应该正确映射包车组合查询参数', async () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 设置出发地和目的地
+      const startLocationButton = screen.getByText('出发地').closest('button')
+      fireEvent.click(startLocationButton!)
+      const confirmButton = screen.getByTestId('area-picker-confirm')
+      fireEvent.click(confirmButton)
+
+      const endLocationButton = screen.getByText('目的地').closest('button')
+      fireEvent.click(endLocationButton!)
+      fireEvent.click(confirmButton)
+
+      // 等待状态更新
+      await waitFor(() => {
+        expect(screen.getAllByText('北京市 北京市 朝阳区')).toHaveLength(2)
+      })
+
+      // 选择包车
+      const charterOption = screen.getByText('包车')
+      fireEvent.click(charterOption)
+
+      // 点击查询
+      const searchButton = screen.getByText('查询路线')
+      fireEvent.click(searchButton)
+
+      // 验证导航参数 - 包车支持大巴和商务车两种车型
+      expect(mockNavigateTo).toHaveBeenCalledWith({
+        url: expect.stringContaining('vehicleType=bus,business')
+      })
+      expect(mockNavigateTo).toHaveBeenCalledWith({
+        url: expect.stringContaining('travelMode=charter')
+      })
+    })
+
+    test('应该正确显示组合查询选项', () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 验证三个组合查询选项都存在
+      expect(screen.getByText('大巴拼车')).toBeInTheDocument()
+      expect(screen.getByText('商务车')).toBeInTheDocument()
+      expect(screen.getByText('包车')).toBeInTheDocument()
+    })
+
+    test('应该正确显示组合查询选项的选中状态', () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 默认选中大巴拼车
+      const busOption = screen.getByText('大巴拼车')
+      expect(busOption.parentElement).toHaveClass('bg-gradient-to-r')
+      expect(busOption.parentElement).toHaveClass('text-white')
+
+      // 选择商务车
+      const businessOption = screen.getByText('商务车')
+      fireEvent.click(businessOption)
+
+      // 验证商务车被选中,大巴拼车取消选中
+      expect(businessOption.parentElement).toHaveClass('bg-gradient-to-r')
+      expect(businessOption.parentElement).toHaveClass('text-white')
+      expect(busOption.parentElement).not.toHaveClass('text-white')
+    })
+  })
+
+  describe('组合查询参数传递', () => {
+    test('应该正确传递完整的组合查询参数', async () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 设置出发地和目的地
+      const startLocationButton = screen.getByText('出发地').closest('button')
+      fireEvent.click(startLocationButton!)
+      const confirmButton = screen.getByTestId('area-picker-confirm')
+      fireEvent.click(confirmButton)
+
+      const endLocationButton = screen.getByText('目的地').closest('button')
+      fireEvent.click(endLocationButton!)
+      fireEvent.click(confirmButton)
+
+      // 等待状态更新
+      await waitFor(() => {
+        expect(screen.getAllByText('北京市 北京市 朝阳区')).toHaveLength(2)
+      })
+
+      // 选择包车
+      const charterOption = screen.getByText('包车')
+      fireEvent.click(charterOption)
+
+      // 点击查询
+      const searchButton = screen.getByText('查询路线')
+      fireEvent.click(searchButton)
+
+      // 验证完整的导航参数
+      const navigateCall = mockNavigateTo.mock.calls[0][0]
+      const url = navigateCall.url
+
+      expect(url).toContain('startAreaIds=[1,11,101]')
+      expect(url).toContain('endAreaIds=[1,11,101]')
+      expect(url).toContain('date=')
+      expect(url).toContain('vehicleType=bus,business')
+      expect(url).toContain('travelMode=charter')
+    })
+
+    test('应该正确传递商务车多值出行方式参数', async () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 设置出发地和目的地
+      const startLocationButton = screen.getByText('出发地').closest('button')
+      fireEvent.click(startLocationButton!)
+      const confirmButton = screen.getByTestId('area-picker-confirm')
+      fireEvent.click(confirmButton)
+
+      const endLocationButton = screen.getByText('目的地').closest('button')
+      fireEvent.click(endLocationButton!)
+      fireEvent.click(confirmButton)
+
+      // 等待状态更新
+      await waitFor(() => {
+        expect(screen.getAllByText('北京市 北京市 朝阳区')).toHaveLength(2)
+      })
+
+      // 选择商务车
+      const businessOption = screen.getByText('商务车')
+      fireEvent.click(businessOption)
+
+      // 点击查询
+      const searchButton = screen.getByText('查询路线')
+      fireEvent.click(searchButton)
+
+      // 验证商务车支持多值出行方式参数
+      const navigateCall = mockNavigateTo.mock.calls[0][0]
+      const url = navigateCall.url
+
+      expect(url).toContain('vehicleType=business')
+      expect(url).toContain('travelMode=carpool,charter')
+    })
+
+    test('应该正确传递包车多值车型参数', async () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 设置出发地和目的地
+      const startLocationButton = screen.getByText('出发地').closest('button')
+      fireEvent.click(startLocationButton!)
+      const confirmButton = screen.getByTestId('area-picker-confirm')
+      fireEvent.click(confirmButton)
+
+      const endLocationButton = screen.getByText('目的地').closest('button')
+      fireEvent.click(endLocationButton!)
+      fireEvent.click(confirmButton)
+
+      // 等待状态更新
+      await waitFor(() => {
+        expect(screen.getAllByText('北京市 北京市 朝阳区')).toHaveLength(2)
+      })
+
+      // 选择包车
+      const charterOption = screen.getByText('包车')
+      fireEvent.click(charterOption)
+
+      // 点击查询
+      const searchButton = screen.getByText('查询路线')
+      fireEvent.click(searchButton)
+
+      // 验证包车支持多值车型参数
+      const navigateCall = mockNavigateTo.mock.calls[0][0]
+      const url = navigateCall.url
+
+      expect(url).toContain('vehicleType=bus,business')
+      expect(url).toContain('travelMode=charter')
+    })
+  })
+
+  describe('组合查询样式验证', () => {
+    test('应该为不同组合查询选项应用正确的样式', () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      const busOption = screen.getByText('大巴拼车')
+      const businessOption = screen.getByText('商务车')
+      const charterOption = screen.getByText('包车')
+
+      // 默认选中大巴拼车
+      expect(busOption.parentElement).toHaveClass('bg-gradient-to-r')
+      expect(busOption.parentElement).toHaveClass('text-white')
+
+      // 选择商务车
+      fireEvent.click(businessOption)
+      expect(businessOption.parentElement).toHaveClass('bg-gradient-to-r')
+      expect(businessOption.parentElement).toHaveClass('text-white')
+
+      // 选择包车
+      fireEvent.click(charterOption)
+      expect(charterOption.parentElement).toHaveClass('bg-gradient-to-r')
+      expect(charterOption.parentElement).toHaveClass('text-white')
+    })
+
+    test('应该正确切换组合查询选项的选中状态', () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      const busOption = screen.getByText('大巴拼车')
+      const businessOption = screen.getByText('商务车')
+      const charterOption = screen.getByText('包车')
+
+      // 初始状态:大巴拼车选中
+      expect(busOption.parentElement).toHaveClass('text-white')
+      expect(businessOption.parentElement).not.toHaveClass('text-white')
+      expect(charterOption.parentElement).not.toHaveClass('text-white')
+
+      // 选择商务车
+      fireEvent.click(businessOption)
+      expect(busOption.parentElement).not.toHaveClass('text-white')
+      expect(businessOption.parentElement).toHaveClass('text-white')
+      expect(charterOption.parentElement).not.toHaveClass('text-white')
+
+      // 选择包车
+      fireEvent.click(charterOption)
+      expect(busOption.parentElement).not.toHaveClass('text-white')
+      expect(businessOption.parentElement).not.toHaveClass('text-white')
+      expect(charterOption.parentElement).toHaveClass('text-white')
+    })
+  })
+
+  describe('组合查询边界情况', () => {
+    test('应该正确处理默认组合查询参数', () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 验证默认参数
+      const busOption = screen.getByText('大巴拼车')
+      expect(busOption.parentElement).toHaveClass('text-white')
+
+      // 验证默认参数设置
+      expect(mockNavigateTo).not.toHaveBeenCalled()
+    })
+
+    test('应该正确处理组合查询参数变化', async () => {
+      render(
+        <Wrapper>
+          <HomePage />
+        </Wrapper>
+      )
+
+      // 设置出发地和目的地
+      const startLocationButton = screen.getByText('出发地').closest('button')
+      fireEvent.click(startLocationButton!)
+      const confirmButton = screen.getByTestId('area-picker-confirm')
+      fireEvent.click(confirmButton)
+
+      const endLocationButton = screen.getByText('目的地').closest('button')
+      fireEvent.click(endLocationButton!)
+      fireEvent.click(confirmButton)
+
+      // 等待状态更新
+      await waitFor(() => {
+        expect(screen.getAllByText('北京市 北京市 朝阳区')).toHaveLength(2)
+      })
+
+      // 测试从大巴拼车切换到商务车
+      const busOption = screen.getByText('大巴拼车')
+      const businessOption = screen.getByText('商务车')
+
+      // 初始选择大巴拼车
+      expect(busOption.parentElement).toHaveClass('text-white')
+
+      // 切换到商务车
+      fireEvent.click(businessOption)
+      expect(businessOption.parentElement).toHaveClass('text-white')
+      expect(busOption.parentElement).not.toHaveClass('text-white')
+
+      // 点击查询
+      const searchButton = screen.getByText('查询路线')
+      fireEvent.click(searchButton)
+
+      // 验证商务车参数
+      const navigateCall = mockNavigateTo.mock.calls[0][0]
+      const url = navigateCall.url
+
+      expect(url).toContain('vehicleType=business')
+      expect(url).toContain('travelMode=carpool,charter')
+      expect(url).not.toContain('vehicleType=bus')
+      expect(url).not.toContain('travelMode=carpool')
+    })
+  })
+})

+ 428 - 0
tests/e2e/specs/travel-flow/combination-query.spec.ts

@@ -0,0 +1,428 @@
+import { test, expect } from '../../utils/test-setup';
+
+test.describe('组合查询流程 E2E 测试', () => {
+  test.beforeEach(async ({ page }) => {
+    // 确保数据库中有测试数据
+    await page.goto('/');
+  });
+
+  test('完整组合查询流程 - 大巴拼车', async ({ page }) => {
+    // 1. 访问首页
+    await page.goto('/');
+    await expect(page.locator('text=便捷出行')).toBeVisible();
+
+    // 2. 验证默认选择大巴拼车
+    const busOption = page.locator('text=大巴拼车').first();
+    await expect(busOption).toBeVisible();
+    await expect(busOption).toHaveClass(/bg-gradient-to-r/);
+
+    // 3. 设置出发地
+    const startLocationButton = page.locator('button:has-text("出发地")').first();
+    await startLocationButton.click();
+
+    // 等待地区选择器显示
+    await expect(page.locator('text=选择出发地')).toBeVisible();
+
+    // 选择地区(这里使用模拟的地区选择)
+    const confirmButton = page.locator('button:has-text("确认")').first();
+    await confirmButton.click();
+
+    // 4. 设置目的地
+    const endLocationButton = page.locator('button:has-text("目的地")').first();
+    await endLocationButton.click();
+
+    await expect(page.locator('text=选择目的地')).toBeVisible();
+    await confirmButton.click();
+
+    // 5. 验证地区选择成功
+    await expect(page.locator('text=北京市 北京市 朝阳区')).toBeVisible();
+
+    // 6. 点击查询按钮
+    const searchButton = page.locator('button:has-text("查询路线")').first();
+    await searchButton.click();
+
+    // 7. 验证导航到活动选择页面
+    await expect(page).toHaveURL(/.*pages\/select-activity\/ActivitySelectPage/);
+
+    // 8. 验证查询参数传递正确
+    const currentUrl = page.url();
+    expect(currentUrl).toContain('vehicleType=bus');
+    expect(currentUrl).toContain('travelMode=carpool');
+    expect(currentUrl).toContain('startAreaIds=');
+    expect(currentUrl).toContain('endAreaIds=');
+
+    // 9. 验证活动选择页面显示
+    await expect(page.locator('text=选择活动')).toBeVisible();
+
+    // 10. 选择第一个活动
+    const firstActivity = page.locator('[data-testid="activity-card"]').first();
+    await firstActivity.click();
+
+    // 11. 验证导航到班次列表页面
+    await expect(page).toHaveURL(/.*pages\/schedule-list\/ScheduleListPage/);
+
+    // 12. 验证班次列表页面显示
+    await expect(page.locator('text=路线列表')).toBeVisible();
+
+    // 13. 验证路线卡片显示车型和出行方式
+    const routeCard = page.locator('[data-testid="route-card"]').first();
+    await expect(routeCard).toBeVisible();
+
+    // 验证包含车型信息
+    await expect(routeCard).toContainText(/大巴|中巴|小车|商务车/);
+
+    // 验证包含出行方式信息
+    await expect(routeCard).toContainText(/拼车|包车/);
+  });
+
+  test('完整组合查询流程 - 商务车', async ({ page }) => {
+    // 1. 访问首页
+    await page.goto('/');
+    await expect(page.locator('text=便捷出行')).toBeVisible();
+
+    // 2. 选择商务车
+    const businessOption = page.locator('text=商务车').first();
+    await businessOption.click();
+    await expect(businessOption).toHaveClass(/bg-gradient-to-r/);
+
+    // 3. 设置出发地
+    const startLocationButton = page.locator('button:has-text("出发地")').first();
+    await startLocationButton.click();
+
+    // 等待地区选择器显示
+    await expect(page.locator('text=选择出发地')).toBeVisible();
+
+    // 选择地区
+    const confirmButton = page.locator('button:has-text("确认")').first();
+    await confirmButton.click();
+
+    // 4. 设置目的地
+    const endLocationButton = page.locator('button:has-text("目的地")').first();
+    await endLocationButton.click();
+
+    await expect(page.locator('text=选择目的地')).toBeVisible();
+    await confirmButton.click();
+
+    // 5. 点击查询按钮
+    const searchButton = page.locator('button:has-text("查询路线")').first();
+    await searchButton.click();
+
+    // 6. 验证导航到活动选择页面
+    await expect(page).toHaveURL(/.*pages\/select-activity\/ActivitySelectPage/);
+
+    // 7. 验证商务车组合查询参数
+    const currentUrl = page.url();
+    expect(currentUrl).toContain('vehicleType=business');
+    expect(currentUrl).toContain('travelMode=carpool,charter');
+
+    // 8. 选择第一个活动
+    const firstActivity = page.locator('[data-testid="activity-card"]').first();
+    await firstActivity.click();
+
+    // 9. 验证导航到班次列表页面
+    await expect(page).toHaveURL(/.*pages\/schedule-list\/ScheduleListPage/);
+
+    // 10. 验证班次列表页面显示
+    await expect(page.locator('text=路线列表')).toBeVisible();
+
+    // 11. 验证路线卡片显示商务车信息
+    const routeCard = page.locator('[data-testid="route-card"]').first();
+    await expect(routeCard).toBeVisible();
+
+    // 验证包含商务车型信息
+    await expect(routeCard).toContainText(/商务车/);
+  });
+
+  test('完整组合查询流程 - 包车', async ({ page }) => {
+    // 1. 访问首页
+    await page.goto('/');
+    await expect(page.locator('text=便捷出行')).toBeVisible();
+
+    // 2. 选择包车
+    const charterOption = page.locator('text=包车').first();
+    await charterOption.click();
+    await expect(charterOption).toHaveClass(/bg-gradient-to-r/);
+
+    // 3. 设置出发地
+    const startLocationButton = page.locator('button:has-text("出发地")').first();
+    await startLocationButton.click();
+
+    // 等待地区选择器显示
+    await expect(page.locator('text=选择出发地')).toBeVisible();
+
+    // 选择地区
+    const confirmButton = page.locator('button:has-text("确认")').first();
+    await confirmButton.click();
+
+    // 4. 设置目的地
+    const endLocationButton = page.locator('button:has-text("目的地")').first();
+    await endLocationButton.click();
+
+    await expect(page.locator('text=选择目的地')).toBeVisible();
+    await confirmButton.click();
+
+    // 5. 点击查询按钮
+    const searchButton = page.locator('button:has-text("查询路线")').first();
+    await searchButton.click();
+
+    // 6. 验证导航到活动选择页面
+    await expect(page).toHaveURL(/.*pages\/select-activity\/ActivitySelectPage/);
+
+    // 7. 验证包车组合查询参数
+    const currentUrl = page.url();
+    expect(currentUrl).toContain('vehicleType=bus,business');
+    expect(currentUrl).toContain('travelMode=charter');
+
+    // 8. 选择第一个活动
+    const firstActivity = page.locator('[data-testid="activity-card"]').first();
+    await firstActivity.click();
+
+    // 9. 验证导航到班次列表页面
+    await expect(page).toHaveURL(/.*pages\/schedule-list\/ScheduleListPage/);
+
+    // 10. 验证班次列表页面显示
+    await expect(page.locator('text=路线列表')).toBeVisible();
+
+    // 11. 验证路线卡片显示包车信息
+    const routeCard = page.locator('[data-testid="route-card"]').first();
+    await expect(routeCard).toBeVisible();
+
+    // 验证包含包车出行方式信息
+    await expect(routeCard).toContainText(/包车/);
+  });
+
+  test('组合查询选项切换', async ({ page }) => {
+    // 1. 访问首页
+    await page.goto('/');
+
+    // 2. 验证默认选择大巴拼车
+    const busOption = page.locator('text=大巴拼车').first();
+    const businessOption = page.locator('text=商务车').first();
+    const charterOption = page.locator('text=包车').first();
+
+    await expect(busOption).toHaveClass(/bg-gradient-to-r/);
+    await expect(businessOption).not.toHaveClass(/text-white/);
+    await expect(charterOption).not.toHaveClass(/text-white/);
+
+    // 3. 切换到商务车
+    await businessOption.click();
+    await expect(busOption).not.toHaveClass(/text-white/);
+    await expect(businessOption).toHaveClass(/bg-gradient-to-r/);
+    await expect(charterOption).not.toHaveClass(/text-white/);
+
+    // 4. 切换到包车
+    await charterOption.click();
+    await expect(busOption).not.toHaveClass(/text-white/);
+    await expect(businessOption).not.toHaveClass(/text-white/);
+    await expect(charterOption).toHaveClass(/bg-gradient-to-r/);
+
+    // 5. 切换回大巴拼车
+    await busOption.click();
+    await expect(busOption).toHaveClass(/bg-gradient-to-r/);
+    await expect(businessOption).not.toHaveClass(/text-white/);
+    await expect(charterOption).not.toHaveClass(/text-white/);
+  });
+
+  test('组合查询参数验证', async ({ page }) => {
+    // 1. 访问首页
+    await page.goto('/');
+
+    // 2. 设置出发地和目的地
+    const startLocationButton = page.locator('button:has-text("出发地")').first();
+    await startLocationButton.click();
+
+    const confirmButton = page.locator('button:has-text("确认")').first();
+    await confirmButton.click();
+
+    const endLocationButton = page.locator('button:has-text("目的地")').first();
+    await endLocationButton.click();
+    await confirmButton.click();
+
+    // 3. 测试大巴拼车参数
+    const busOption = page.locator('text=大巴拼车').first();
+    await busOption.click();
+
+    const searchButton = page.locator('button:has-text("查询路线")').first();
+    await searchButton.click();
+
+    let currentUrl = page.url();
+    expect(currentUrl).toContain('vehicleType=bus');
+    expect(currentUrl).toContain('travelMode=carpool');
+
+    // 4. 返回首页测试商务车参数
+    await page.goBack();
+
+    const businessOption = page.locator('text=商务车').first();
+    await businessOption.click();
+    await searchButton.click();
+
+    currentUrl = page.url();
+    expect(currentUrl).toContain('vehicleType=business');
+    expect(currentUrl).toContain('travelMode=carpool,charter');
+
+    // 5. 返回首页测试包车参数
+    await page.goBack();
+
+    const charterOption = page.locator('text=包车').first();
+    await charterOption.click();
+    await searchButton.click();
+
+    currentUrl = page.url();
+    expect(currentUrl).toContain('vehicleType=bus,business');
+    expect(currentUrl).toContain('travelMode=charter');
+  });
+
+  test('组合查询结果验证', async ({ page }) => {
+    // 1. 访问首页并设置查询条件
+    await page.goto('/');
+
+    // 设置出发地和目的地
+    const startLocationButton = page.locator('button:has-text("出发地")').first();
+    await startLocationButton.click();
+
+    const confirmButton = page.locator('button:has-text("确认")').first();
+    await confirmButton.click();
+
+    const endLocationButton = page.locator('button:has-text("目的地")').first();
+    await endLocationButton.click();
+    await confirmButton.click();
+
+    // 2. 测试大巴拼车查询结果
+    const busOption = page.locator('text=大巴拼车').first();
+    await busOption.click();
+
+    const searchButton = page.locator('button:has-text("查询路线")').first();
+    await searchButton.click();
+
+    // 等待活动选择页面加载
+    await expect(page.locator('text=选择活动')).toBeVisible();
+
+    // 验证活动列表显示
+    const activityCards = page.locator('[data-testid="activity-card"]');
+    await expect(activityCards.first()).toBeVisible();
+
+    // 3. 选择活动并验证路线列表
+    await activityCards.first().click();
+
+    // 等待班次列表页面加载
+    await expect(page.locator('text=路线列表')).toBeVisible();
+
+    // 验证路线列表显示
+    const routeCards = page.locator('[data-testid="route-card"]');
+    await expect(routeCards.first()).toBeVisible();
+
+    // 4. 验证路线信息包含车型和出行方式
+    const firstRouteCard = routeCards.first();
+    const routeText = await firstRouteCard.textContent();
+
+    // 验证包含车型信息
+    expect(routeText).toMatch(/大巴|中巴|小车|商务车/);
+
+    // 验证包含出行方式信息
+    expect(routeText).toMatch(/拼车|包车/);
+
+    // 5. 验证路线详情显示完整信息
+    await firstRouteCard.click();
+
+    // 验证路线详情页面显示
+    await expect(page.locator('text=路线详情')).toBeVisible();
+
+    // 验证详情页面包含车型和出行方式信息
+    const detailText = await page.textContent('body');
+    expect(detailText).toMatch(/车型.*[大巴|中巴|小车|商务车]/);
+    expect(detailText).toMatch(/出行方式.*[拼车|包车]/);
+  });
+
+  test('组合查询错误处理', async ({ page }) => {
+    // 1. 访问首页
+    await page.goto('/');
+
+    // 2. 不设置出发地和目的地,直接点击查询
+    const searchButton = page.locator('button:has-text("查询路线")').first();
+    await searchButton.click();
+
+    // 3. 验证页面没有跳转(应该停留在首页)
+    await expect(page).toHaveURL(/\/$/);
+
+    // 4. 验证错误提示显示(如果有的话)
+    // 这里可以根据实际实现检查是否有错误提示
+
+    // 5. 只设置出发地,不设置目的地
+    const startLocationButton = page.locator('button:has-text("出发地")').first();
+    await startLocationButton.click();
+
+    const confirmButton = page.locator('button:has-text("确认")').first();
+    await confirmButton.click();
+
+    // 6. 再次点击查询
+    await searchButton.click();
+
+    // 7. 验证页面仍然没有跳转
+    await expect(page).toHaveURL(/\/$/);
+
+    // 8. 设置目的地
+    const endLocationButton = page.locator('button:has-text("目的地")').first();
+    await endLocationButton.click();
+    await confirmButton.click();
+
+    // 9. 现在应该可以正常查询
+    await searchButton.click();
+
+    // 10. 验证页面跳转到活动选择页面
+    await expect(page).toHaveURL(/.*pages\/select-activity\/ActivitySelectPage/);
+  });
+
+  test('组合查询性能测试', async ({ page }) => {
+    // 1. 访问首页
+    await page.goto('/');
+
+    // 2. 设置查询条件
+    const startLocationButton = page.locator('button:has-text("出发地")').first();
+    await startLocationButton.click();
+
+    const confirmButton = page.locator('button:has-text("确认")').first();
+    await confirmButton.click();
+
+    const endLocationButton = page.locator('button:has-text("目的地")').first();
+    await endLocationButton.click();
+    await confirmButton.click();
+
+    // 3. 测试不同组合查询的响应时间
+    const travelOptions = [
+      { name: '大巴拼车', expectedParams: { vehicleType: 'bus', travelMode: 'carpool' } },
+      { name: '商务车', expectedParams: { vehicleType: 'business', travelMode: 'carpool,charter' } },
+      { name: '包车', expectedParams: { vehicleType: 'bus,business', travelMode: 'charter' } }
+    ];
+
+    for (const option of travelOptions) {
+      // 选择出行方式
+      const optionElement = page.locator(`text=${option.name}`).first();
+      await optionElement.click();
+
+      // 记录开始时间
+      const startTime = Date.now();
+
+      // 点击查询
+      const searchButton = page.locator('button:has-text("查询路线")').first();
+      await searchButton.click();
+
+      // 等待页面加载完成
+      await expect(page).toHaveURL(/.*pages\/select-activity\/ActivitySelectPage/);
+
+      // 记录结束时间
+      const endTime = Date.now();
+      const responseTime = endTime - startTime;
+
+      // 验证响应时间在合理范围内(小于5秒)
+      expect(responseTime).toBeLessThan(5000);
+
+      // 验证查询参数正确
+      const currentUrl = page.url();
+      expect(currentUrl).toContain(`vehicleType=${option.expectedParams.vehicleType}`);
+      expect(currentUrl).toContain(`travelMode=${option.expectedParams.travelMode}`);
+
+      // 返回首页进行下一个测试
+      await page.goBack();
+    }
+  });
+});

+ 444 - 0
tests/integration/server/routes/search.integration.test.ts

@@ -0,0 +1,444 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooks,
+  TestDataFactory
+} from '~/utils/server/integration-test-db';
+import { IntegrationTestAssertions } from '~/utils/server/integration-test-utils';
+import { routesRoutesExport } from '@/server/api';
+import { VehicleType, TravelMode } from '@/server/modules/routes/route.schema';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooks()
+
+describe('用户端路线搜索API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof routesRoutesExport>>['api']['v1'];
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(routesRoutesExport).api.v1;
+
+    // 创建测试数据
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) throw new Error('Database not initialized');
+
+    // 创建测试活动
+    const activity1 = await TestDataFactory.createTestActivity(dataSource, {
+      name: '测试活动1'
+    });
+    const activity2 = await TestDataFactory.createTestActivity(dataSource, {
+      name: '测试活动2'
+    });
+
+    // 创建测试路线,覆盖所有组合查询场景
+    // 大巴拼车路线
+    await TestDataFactory.createTestRoute(dataSource, {
+      name: '大巴拼车路线1',
+      vehicleType: VehicleType.BUS,
+      travelMode: TravelMode.CARPOOL,
+      activityId: activity1.id,
+      price: 100,
+      availableSeats: 30
+    });
+
+    await TestDataFactory.createTestRoute(dataSource, {
+      name: '大巴拼车路线2',
+      vehicleType: VehicleType.BUS,
+      travelMode: TravelMode.CARPOOL,
+      activityId: activity2.id,
+      price: 120,
+      availableSeats: 25
+    });
+
+    // 商务车路线(拼车)
+    await TestDataFactory.createTestRoute(dataSource, {
+      name: '商务车拼车路线',
+      vehicleType: VehicleType.BUSINESS,
+      travelMode: TravelMode.CARPOOL,
+      activityId: activity1.id,
+      price: 200,
+      availableSeats: 4
+    });
+
+    // 商务车路线(包车)
+    await TestDataFactory.createTestRoute(dataSource, {
+      name: '商务车包车路线',
+      vehicleType: VehicleType.BUSINESS,
+      travelMode: TravelMode.CHARTER,
+      activityId: activity2.id,
+      price: 500,
+      availableSeats: 4
+    });
+
+    // 大巴包车路线
+    await TestDataFactory.createTestRoute(dataSource, {
+      name: '大巴包车路线',
+      vehicleType: VehicleType.BUS,
+      travelMode: TravelMode.CHARTER,
+      activityId: activity1.id,
+      price: 800,
+      availableSeats: 40
+    });
+
+    // 中巴拼车路线(用于验证排除逻辑)
+    await TestDataFactory.createTestRoute(dataSource, {
+      name: '中巴拼车路线',
+      vehicleType: VehicleType.MINIBUS,
+      travelMode: TravelMode.CARPOOL,
+      activityId: activity1.id,
+      price: 150,
+      availableSeats: 15
+    });
+
+    // 小车包车路线(用于验证排除逻辑)
+    await TestDataFactory.createTestRoute(dataSource, {
+      name: '小车包车路线',
+      vehicleType: VehicleType.CAR,
+      travelMode: TravelMode.CHARTER,
+      activityId: activity2.id,
+      price: 300,
+      availableSeats: 3
+    });
+  });
+
+  describe('组合查询逻辑测试', () => {
+    it('应该正确查询大巴拼车组合', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus',
+          travelMode: 'carpool'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+        expect(responseData.data.routes).toHaveLength(2);
+
+        // 验证所有返回路线都是大巴拼车
+        responseData.data.routes.forEach((route: any) => {
+          expect(route.vehicleType).toBe(VehicleType.BUS);
+          expect(route.travelMode).toBe(TravelMode.CARPOOL);
+        });
+
+        // 验证包含正确的路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('大巴拼车路线1');
+        expect(routeNames).toContain('大巴拼车路线2');
+
+        // 验证不包含其他类型的路线
+        expect(routeNames).not.toContain('商务车拼车路线');
+        expect(routeNames).not.toContain('大巴包车路线');
+      }
+    });
+
+    it('应该正确查询商务车组合(支持拼车和包车)', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'business',
+          travelMode: 'carpool,charter'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+        expect(responseData.data.routes).toHaveLength(2);
+
+        // 验证所有返回路线都是商务车
+        responseData.data.routes.forEach((route: any) => {
+          expect(route.vehicleType).toBe(VehicleType.BUSINESS);
+        });
+
+        // 验证包含商务车拼车和包车路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('商务车拼车路线');
+        expect(routeNames).toContain('商务车包车路线');
+
+        // 验证不包含其他车型的路线
+        expect(routeNames).not.toContain('大巴拼车路线1');
+        expect(routeNames).not.toContain('中巴拼车路线');
+      }
+    });
+
+    it('应该正确查询包车组合(支持大巴和商务车)', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus,business',
+          travelMode: 'charter'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+        expect(responseData.data.routes).toHaveLength(2);
+
+        // 验证所有返回路线都是包车
+        responseData.data.routes.forEach((route: any) => {
+          expect(route.travelMode).toBe(TravelMode.CHARTER);
+        });
+
+        // 验证包含大巴包车和商务车包车路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('大巴包车路线');
+        expect(routeNames).toContain('商务车包车路线');
+
+        // 验证不包含拼车路线
+        expect(routeNames).not.toContain('大巴拼车路线1');
+        expect(routeNames).not.toContain('商务车拼车路线');
+      }
+    });
+
+    it('应该支持多值车型参数查询', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus,minibus',
+          travelMode: 'carpool'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证包含大巴拼车和中巴拼车路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('大巴拼车路线1');
+        expect(routeNames).toContain('大巴拼车路线2');
+        expect(routeNames).toContain('中巴拼车路线');
+
+        // 验证不包含商务车和小车路线
+        expect(routeNames).not.toContain('商务车拼车路线');
+        expect(routeNames).not.toContain('小车包车路线');
+      }
+    });
+
+    it('应该支持多值出行方式参数查询', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'business',
+          travelMode: 'carpool,charter'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证包含商务车拼车和包车路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('商务车拼车路线');
+        expect(routeNames).toContain('商务车包车路线');
+      }
+    });
+
+    it('应该正确处理单个参数查询', async () => {
+      // 只查询车型
+      const response1 = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response1, 200);
+      if (response1.status === 200) {
+        const responseData = await response1.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证包含所有大巴路线(拼车和包车)
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('大巴拼车路线1');
+        expect(routeNames).toContain('大巴拼车路线2');
+        expect(routeNames).toContain('大巴包车路线');
+      }
+
+      // 只查询出行方式
+      const response2 = await client.routes.search.$get({
+        query: {
+          travelMode: 'carpool'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response2, 200);
+      if (response2.status === 200) {
+        const responseData = await response2.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证包含所有拼车路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('大巴拼车路线1');
+        expect(routeNames).toContain('大巴拼车路线2');
+        expect(routeNames).toContain('商务车拼车路线');
+        expect(routeNames).toContain('中巴拼车路线');
+      }
+    });
+
+    it('应该正确处理空参数查询', async () => {
+      const response = await client.routes.search.$get({
+        query: {}
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证返回所有路线
+        expect(responseData.data.routes.length).toBeGreaterThan(0);
+      }
+    });
+
+    it('应该正确处理无效参数', async () => {
+      // 无效车型参数
+      const response1 = await client.routes.search.$get({
+        query: {
+          vehicleType: 'invalid_vehicle'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response1, 200);
+      if (response1.status === 200) {
+        const responseData = await response1.json();
+        expect(responseData.success).toBe(true);
+        // 无效参数应该被忽略,返回所有路线
+        expect(responseData.data.routes.length).toBeGreaterThan(0);
+      }
+
+      // 无效出行方式参数
+      const response2 = await client.routes.search.$get({
+        query: {
+          travelMode: 'invalid_mode'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response2, 200);
+      if (response2.status === 200) {
+        const responseData = await response2.json();
+        expect(responseData.success).toBe(true);
+        // 无效参数应该被忽略,返回所有路线
+        expect(responseData.data.routes.length).toBeGreaterThan(0);
+      }
+    });
+  });
+
+  describe('组合查询与其他筛选条件结合测试', () => {
+    it('应该支持组合查询与价格筛选结合', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus',
+          travelMode: 'carpool',
+          minPrice: '110',
+          maxPrice: '130'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证只返回价格在110-130之间的大巴拼车路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('大巴拼车路线2'); // 价格120
+        expect(routeNames).not.toContain('大巴拼车路线1'); // 价格100
+
+        // 验证价格范围
+        responseData.data.routes.forEach((route: any) => {
+          expect(route.price).toBeGreaterThanOrEqual(110);
+          expect(route.price).toBeLessThanOrEqual(130);
+        });
+      }
+    });
+
+    it('应该支持组合查询与活动筛选结合', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const activity = await TestDataFactory.createTestActivity(dataSource, {
+        name: '特定活动'
+      });
+
+      // 为特定活动创建路线
+      await TestDataFactory.createTestRoute(dataSource, {
+        name: '特定活动大巴拼车路线',
+        vehicleType: VehicleType.BUS,
+        travelMode: TravelMode.CARPOOL,
+        activityId: activity.id,
+        price: 150
+      });
+
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus',
+          travelMode: 'carpool'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证包含特定活动的路线
+        const routeNames = responseData.data.routes.map((route: any) => route.name);
+        expect(routeNames).toContain('特定活动大巴拼车路线');
+      }
+    });
+  });
+
+  describe('分页和排序测试', () => {
+    it('应该支持组合查询的分页', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus',
+          travelMode: 'carpool',
+          page: '1',
+          pageSize: '1'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证分页信息
+        expect(responseData.data.pagination.page).toBe(1);
+        expect(responseData.data.pagination.pageSize).toBe(1);
+        expect(responseData.data.pagination.total).toBe(2);
+        expect(responseData.data.pagination.totalPages).toBe(2);
+
+        // 验证只返回1条路线
+        expect(responseData.data.routes).toHaveLength(1);
+      }
+    });
+
+    it('应该支持组合查询的价格排序', async () => {
+      const response = await client.routes.search.$get({
+        query: {
+          vehicleType: 'bus',
+          travelMode: 'carpool',
+          sortBy: 'price',
+          sortOrder: 'ASC'
+        }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+      if (response.status === 200) {
+        const responseData = await response.json();
+        expect(responseData.success).toBe(true);
+
+        // 验证路线按价格升序排列
+        const prices = responseData.data.routes.map((route: any) => route.price);
+        expect(prices[0]).toBeLessThanOrEqual(prices[1]);
+      }
+    });
+  });
+});