fix: search bug
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
StatusBar as RNStatusBar,
|
StatusBar as RNStatusBar,
|
||||||
@@ -53,6 +53,9 @@ export default function SearchWorksResultsScreen() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const params = useLocalSearchParams()
|
const params = useLocalSearchParams()
|
||||||
const [searchText, setSearchText] = useState((params.q as string) || '')
|
const [searchText, setSearchText] = useState((params.q as string) || '')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [allWorks, setAllWorks] = useState<WorksSearchResult[]>([])
|
||||||
|
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||||
|
|
||||||
const categories: Category[] = [
|
const categories: Category[] = [
|
||||||
t('worksList.all') as Category,
|
t('worksList.all') as Category,
|
||||||
@@ -67,15 +70,44 @@ export default function SearchWorksResultsScreen() {
|
|||||||
const { data, works, isLoading, error } = useWorksSearch({
|
const { data, works, isLoading, error } = useWorksSearch({
|
||||||
keyword: searchText,
|
keyword: searchText,
|
||||||
category: selectedCategory,
|
category: selectedCategory,
|
||||||
page: 1,
|
page,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 当搜索关键词或分类变化时,重置页码和累积数据
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1)
|
||||||
|
setAllWorks([])
|
||||||
|
}, [searchText, selectedCategory])
|
||||||
|
|
||||||
|
// 累积新数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (works && works.length > 0) {
|
||||||
|
if (page === 1) {
|
||||||
|
setAllWorks(works)
|
||||||
|
} else {
|
||||||
|
setAllWorks((prev) => [...prev, ...works])
|
||||||
|
}
|
||||||
|
setIsLoadingMore(false)
|
||||||
|
}
|
||||||
|
}, [works, page])
|
||||||
|
|
||||||
|
// 加载更多函数
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
if (isLoading || isLoadingMore || !data) return
|
||||||
|
|
||||||
|
// 检查是否还有更多数据
|
||||||
|
if (page >= data.totalPages) return
|
||||||
|
|
||||||
|
setIsLoadingMore(true)
|
||||||
|
setPage((prev) => prev + 1)
|
||||||
|
}
|
||||||
|
|
||||||
// 根据分类过滤结果(如果 API 不支持分类筛选,则在前端过滤)
|
// 根据分类过滤结果(如果 API 不支持分类筛选,则在前端过滤)
|
||||||
const filteredWorks =
|
const filteredWorks =
|
||||||
selectedCategory === categories[0]
|
selectedCategory === categories[0]
|
||||||
? works
|
? allWorks
|
||||||
: works.filter((work) => {
|
: allWorks.filter((work) => {
|
||||||
const convertedWork = convertToWorkItem(work)
|
const convertedWork = convertToWorkItem(work)
|
||||||
return convertedWork.category === selectedCategory
|
return convertedWork.category === selectedCategory
|
||||||
})
|
})
|
||||||
@@ -190,6 +222,15 @@ export default function SearchWorksResultsScreen() {
|
|||||||
params: { id: id.toString() },
|
params: { id: id.toString() },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
onEndReached={handleLoadMore}
|
||||||
|
ListFooterComponent={
|
||||||
|
isLoadingMore ? (
|
||||||
|
<View style={styles.loadingMoreContainer} testID="loading-more-indicator">
|
||||||
|
<ActivityIndicator size="small" color="#FF6699" />
|
||||||
|
<Text style={styles.loadingMoreText}>{t('search.loadingMore')}</Text>
|
||||||
|
</View>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -234,4 +275,15 @@ const styles = StyleSheet.create({
|
|||||||
color: '#8A8A8A',
|
color: '#8A8A8A',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
|
loadingMoreContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 20,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
loadingMoreText: {
|
||||||
|
color: '#F5F5F5',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
239
tests/app/searchWorksResults.test.tsx
Normal file
239
tests/app/searchWorksResults.test.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
/**
|
||||||
|
* TDD Phase 1: RED - Write failing tests first
|
||||||
|
*
|
||||||
|
* 测试搜索结果页面的分页功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { render, waitFor, fireEvent } from '@testing-library/react-native'
|
||||||
|
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||||
|
import SearchWorksResultsScreen from '@/app/searchWorksResults'
|
||||||
|
import { useWorksSearch } from '@/hooks/use-works-search'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('expo-router', () => ({
|
||||||
|
useRouter: jest.fn(),
|
||||||
|
useLocalSearchParams: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/hooks/use-works-search', () => ({
|
||||||
|
useWorksSearch: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
i18n: { language: 'zh-CN' },
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/components/SearchBar', () => 'SearchBar')
|
||||||
|
jest.mock('@/components/WorksGallery', () => 'WorksGallery')
|
||||||
|
|
||||||
|
describe('SearchWorksResultsScreen - Pagination', () => {
|
||||||
|
const mockRouter = {
|
||||||
|
push: jest.fn(),
|
||||||
|
back: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
;(useRouter as jest.Mock).mockReturnValue(mockRouter)
|
||||||
|
;(useLocalSearchParams as jest.Mock).mockReturnValue({ q: '测试' })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Load More Functionality', () => {
|
||||||
|
it('should load first page on initial render', () => {
|
||||||
|
const mockUseWorksSearch = useWorksSearch as jest.Mock
|
||||||
|
mockUseWorksSearch.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
createdAt: new Date('2025-01-15'),
|
||||||
|
template: { id: 't1', name: 'Template 1' },
|
||||||
|
status: 'completed',
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 50,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
totalPages: 3,
|
||||||
|
},
|
||||||
|
works: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
createdAt: new Date('2025-01-15'),
|
||||||
|
template: { id: 't1', name: 'Template 1' },
|
||||||
|
status: 'completed',
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<SearchWorksResultsScreen />)
|
||||||
|
|
||||||
|
// 验证初始调用使用 page: 1
|
||||||
|
expect(mockUseWorksSearch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should accumulate results when loading more pages', async () => {
|
||||||
|
const mockUseWorksSearch = useWorksSearch as jest.Mock
|
||||||
|
|
||||||
|
// 第一页数据
|
||||||
|
const page1Data = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
createdAt: new Date('2025-01-15'),
|
||||||
|
template: { id: 't1', name: 'Template 1' },
|
||||||
|
status: 'completed',
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 第二页数据
|
||||||
|
const page2Data = [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
createdAt: new Date('2025-01-14'),
|
||||||
|
template: { id: 't2', name: 'Template 2' },
|
||||||
|
status: 'completed',
|
||||||
|
duration: 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
mockUseWorksSearch.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
data: page1Data,
|
||||||
|
total: 50,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
totalPages: 3,
|
||||||
|
},
|
||||||
|
works: page1Data,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
loadMore: jest.fn(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rerender } = render(<SearchWorksResultsScreen />)
|
||||||
|
|
||||||
|
// 模拟加载第二页
|
||||||
|
mockUseWorksSearch.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
data: [...page1Data, ...page2Data],
|
||||||
|
total: 50,
|
||||||
|
page: 2,
|
||||||
|
limit: 20,
|
||||||
|
totalPages: 3,
|
||||||
|
},
|
||||||
|
works: [...page1Data, ...page2Data],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
loadMore: jest.fn(),
|
||||||
|
})
|
||||||
|
|
||||||
|
rerender(<SearchWorksResultsScreen />)
|
||||||
|
|
||||||
|
// 验证结果累积
|
||||||
|
expect(mockUseWorksSearch).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not load more when already loading', () => {
|
||||||
|
const mockLoadMore = jest.fn()
|
||||||
|
const mockUseWorksSearch = useWorksSearch as jest.Mock
|
||||||
|
|
||||||
|
mockUseWorksSearch.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
data: [],
|
||||||
|
total: 50,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
totalPages: 3,
|
||||||
|
},
|
||||||
|
works: [],
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
loadMore: mockLoadMore,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<SearchWorksResultsScreen />)
|
||||||
|
|
||||||
|
// 验证加载中时不应触发新的加载
|
||||||
|
expect(mockLoadMore).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not load more when all pages loaded', () => {
|
||||||
|
const mockLoadMore = jest.fn()
|
||||||
|
const mockUseWorksSearch = useWorksSearch as jest.Mock
|
||||||
|
|
||||||
|
mockUseWorksSearch.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
createdAt: new Date('2025-01-15'),
|
||||||
|
template: { id: 't1', name: 'Template 1' },
|
||||||
|
status: 'completed',
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
totalPages: 1,
|
||||||
|
},
|
||||||
|
works: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
createdAt: new Date('2025-01-15'),
|
||||||
|
template: { id: 't1', name: 'Template 1' },
|
||||||
|
status: 'completed',
|
||||||
|
duration: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
loadMore: mockLoadMore,
|
||||||
|
hasMore: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<SearchWorksResultsScreen />)
|
||||||
|
|
||||||
|
// 验证没有更多数据时不应触发加载
|
||||||
|
expect(mockLoadMore).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Loading State', () => {
|
||||||
|
it('should show loading indicator when loading more', () => {
|
||||||
|
const mockUseWorksSearch = useWorksSearch as jest.Mock
|
||||||
|
|
||||||
|
mockUseWorksSearch.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
data: [],
|
||||||
|
total: 50,
|
||||||
|
page: 2,
|
||||||
|
limit: 20,
|
||||||
|
totalPages: 3,
|
||||||
|
},
|
||||||
|
works: [],
|
||||||
|
isLoading: true,
|
||||||
|
error: null,
|
||||||
|
isLoadingMore: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { getByTestId } = render(<SearchWorksResultsScreen />)
|
||||||
|
|
||||||
|
// 验证显示加载指示器
|
||||||
|
expect(() => getByTestId('loading-more-indicator')).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user