import React from 'react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { LocationSearch } from '../../src/components/LocationSearch'
// Mock API 客户端
const mockLocationClient = {
$get: jest.fn()
}
jest.mock('../../src/api', () => ({
locationClient: mockLocationClient
}))
// 创建测试用的 QueryClient
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
// 包装组件
const Wrapper = ({ children }: { children: React.ReactNode }) => {
const queryClient = createTestQueryClient()
return (
{children}
)
}
// 模拟数据
const mockLocations = [
{
id: 1,
name: '北京南站',
province: '北京市',
city: '北京市',
district: '丰台区',
address: '北京市丰台区北京南站'
},
{
id: 2,
name: '首都机场',
province: '北京市',
city: '北京市',
district: '顺义区',
address: '北京市顺义区首都机场'
},
{
id: 3,
name: '天安门广场',
province: '北京市',
city: '北京市',
district: '东城区',
address: '北京市东城区天安门广场'
}
]
describe('LocationSearch 组件', () => {
beforeEach(() => {
// 重置所有 mock
jest.clearAllMocks()
// 设置默认的 mock 返回值
mockLocationClient.$get.mockResolvedValue({
status: 200,
json: async () => mockLocations
})
})
test('应该正确渲染组件', () => {
const onChange = jest.fn()
render(
)
// 检查输入框
expect(screen.getByPlaceholderText('搜索地点')).toBeInTheDocument()
})
test('应该显示自定义占位符', () => {
const onChange = jest.fn()
render(
)
expect(screen.getByPlaceholderText('搜索出发地')).toBeInTheDocument()
})
test('应该处理输入变化和防抖搜索', async () => {
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '北京' } })
// 检查输入值
expect(input).toHaveValue('北京')
// 等待防抖时间
await waitFor(() => {
expect(mockLocationClient.$get).toHaveBeenCalledWith({
query: {
keyword: '北京',
provinceId: undefined,
cityId: undefined,
districtId: undefined
}
})
}, { timeout: 500 })
})
test('应该显示搜索结果', async () => {
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '北京' } })
// 等待搜索结果
await waitFor(() => {
expect(screen.getByText('北京南站')).toBeInTheDocument()
expect(screen.getByText('首都机场')).toBeInTheDocument()
expect(screen.getByText('天安门广场')).toBeInTheDocument()
})
// 检查地点信息显示
expect(screen.getByText('北京南站 · 丰台区 · 北京市 · 北京市')).toBeInTheDocument()
expect(screen.getByText('北京市丰台区北京南站')).toBeInTheDocument()
})
test('应该处理地点选择', async () => {
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '北京' } })
// 等待搜索结果
await waitFor(() => {
expect(screen.getByText('北京南站')).toBeInTheDocument()
})
// 选择地点
const locationItem = screen.getByText('北京南站')
fireEvent.click(locationItem)
// 检查 onChange 被调用
expect(onChange).toHaveBeenCalledWith(mockLocations[0])
// 检查输入框值更新
expect(input).toHaveValue('北京南站')
// 检查搜索结果隐藏
expect(screen.queryByText('首都机场')).not.toBeInTheDocument()
})
test('应该处理清除操作', () => {
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '北京' } })
// 检查清除按钮出现
const clearButton = screen.getByText('×')
expect(clearButton).toBeInTheDocument()
// 点击清除按钮
fireEvent.click(clearButton)
// 检查输入框被清空
expect(input).toHaveValue('')
// 检查 onChange 被调用
expect(onChange).toHaveBeenCalledWith(null)
})
test('应该显示当前选择的地点', () => {
const selectedLocation = {
id: 1,
name: '北京南站',
province: '北京市',
city: '北京市',
district: '丰台区',
address: '北京市丰台区北京南站'
}
const onChange = jest.fn()
render(
)
// 检查已选择的地点显示
expect(screen.getByText('已选择: 北京南站 · 丰台区 · 北京市 · 北京市')).toBeInTheDocument()
})
test('应该支持地区筛选', async () => {
const onChange = jest.fn()
const areaFilter = {
provinceId: 1,
cityId: 11,
districtId: 101
}
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '北京' } })
// 等待搜索调用
await waitFor(() => {
expect(mockLocationClient.$get).toHaveBeenCalledWith({
query: {
keyword: '北京',
provinceId: 1,
cityId: 11,
districtId: 101
}
})
}, { timeout: 500 })
})
test('应该显示搜索中状态', async () => {
// 模拟延迟响应
mockLocationClient.$get.mockImplementation(() =>
new Promise(resolve => {
setTimeout(() => {
resolve({
status: 200,
json: async () => mockLocations
})
}, 100)
})
)
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '北京' } })
// 检查搜索中状态
await waitFor(() => {
expect(screen.getByText('搜索中...')).toBeInTheDocument()
})
})
test('应该显示无结果状态', async () => {
// 模拟空结果
mockLocationClient.$get.mockResolvedValue({
status: 200,
json: async () => []
})
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '不存在的地点' } })
// 检查无结果状态
await waitFor(() => {
expect(screen.getByText('未找到相关地点')).toBeInTheDocument()
})
})
test('应该显示请输入关键词状态', () => {
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 聚焦输入框但不输入内容
fireEvent.focus(input)
// 检查提示状态
expect(screen.getByText('请输入地点名称')).toBeInTheDocument()
})
test('应该处理 API 错误', async () => {
// 模拟 API 错误
mockLocationClient.$get.mockResolvedValue({
status: 500,
json: async () => ({ success: false, message: '服务器错误' })
})
const onChange = jest.fn()
render(
)
const input = screen.getByPlaceholderText('搜索地点')
// 输入搜索关键词
fireEvent.input(input, { target: { value: '北京' } })
// 组件应该正常处理错误,不显示搜索结果
await waitFor(() => {
expect(mockLocationClient.$get).toHaveBeenCalled()
})
// 检查没有显示错误信息(组件应该静默处理错误)
expect(screen.queryByText('服务器错误')).not.toBeInTheDocument()
})
})