417 lines
11 KiB
TypeScript
417 lines
11 KiB
TypeScript
/**
|
|
* TDD Phase 1: RED - Write failing tests first
|
|
*
|
|
* This test file follows TDD principles:
|
|
* 1. Tests are written BEFORE implementation
|
|
* 2. Tests describe desired behavior, not implementation
|
|
* 3. Tests should fail initially because Hook doesn't exist
|
|
*/
|
|
|
|
import { renderHook, waitFor, act } from '@testing-library/react-native'
|
|
import { useWorksSearch, type WorksCategory } from '@/hooks/use-works-search'
|
|
import { root } from '@repo/core'
|
|
import { TemplateGenerationController } from '@repo/sdk'
|
|
|
|
// Mock dependencies
|
|
jest.mock('@repo/core', () => ({
|
|
root: {
|
|
get: jest.fn(),
|
|
},
|
|
}))
|
|
|
|
// Mock @tanstack/react-query before importing the hook
|
|
const mockRefetch = jest.fn()
|
|
const mockUseQuery = jest.fn()
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const mockUseQueryImpl = (...args: any[]) => {
|
|
return mockUseQuery(args[0], args[1])
|
|
}
|
|
|
|
jest.mock('@tanstack/react-query', () => ({
|
|
useQuery: mockUseQueryImpl,
|
|
}))
|
|
|
|
describe('useWorksSearch', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks()
|
|
})
|
|
|
|
describe('initial state', () => {
|
|
it('should return initial state with no data when no keyword provided', () => {
|
|
// This test will FAIL initially because useWorksSearch doesn't exist yet
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '' }))
|
|
|
|
expect(result.current.data).toBeUndefined()
|
|
expect(result.current.works).toEqual([])
|
|
expect(result.current.isLoading).toBe(false)
|
|
expect(result.current.error).toBeNull()
|
|
})
|
|
|
|
it('should not execute query when keyword is empty', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
renderHook(() => useWorksSearch({ keyword: '' }))
|
|
|
|
// Verify useQuery was called with enabled: false
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
enabled: false,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should not execute query when keyword is only whitespace', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
renderHook(() => useWorksSearch({ keyword: ' ' }))
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
enabled: false,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('keyword search', () => {
|
|
it('should search works by keyword successfully', async () => {
|
|
const mockData = {
|
|
data: [
|
|
{
|
|
id: '1',
|
|
createdAt: new Date('2025-01-15'),
|
|
template: { id: 'template-1', name: 'Test Template' },
|
|
status: 'completed',
|
|
duration: 5,
|
|
},
|
|
{
|
|
id: '2',
|
|
createdAt: new Date('2025-01-14'),
|
|
template: { id: 'template-2', name: 'Test Template 2' },
|
|
status: 'completed',
|
|
duration: 10,
|
|
},
|
|
],
|
|
total: 2,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
}
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: mockData,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '测试' }))
|
|
|
|
expect(result.current.data).toEqual(mockData)
|
|
expect(result.current.works).toEqual(mockData.data)
|
|
expect(result.current.error).toBeNull()
|
|
expect(result.current.isLoading).toBe(false)
|
|
|
|
// Verify queryKey includes keyword
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queryKey: ['worksSearch', '测试', undefined, 1, 20],
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should handle search with special characters in keyword', () => {
|
|
const mockData = {
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 0,
|
|
}
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: mockData,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '测试@#$%' }))
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queryKey: ['worksSearch', '测试@#$%', undefined, 1, 20],
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should execute query when keyword has content', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: { data: [], total: 0, page: 1, limit: 20, totalPages: 0 },
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
renderHook(() => useWorksSearch({ keyword: 'test' }))
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
enabled: true,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('category filter', () => {
|
|
it('should filter works by category', () => {
|
|
const mockData = {
|
|
data: [
|
|
{
|
|
id: '1',
|
|
createdAt: new Date('2025-01-15'),
|
|
template: { id: 'template-1', name: 'Test Template' },
|
|
status: 'completed',
|
|
duration: 5,
|
|
},
|
|
],
|
|
total: 1,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
}
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: mockData,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() =>
|
|
useWorksSearch({ keyword: '测试', category: '萌宠' })
|
|
)
|
|
|
|
expect(result.current.works).toEqual(mockData.data)
|
|
|
|
// Verify queryKey includes category
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queryKey: ['worksSearch', '测试', '萌宠', 1, 20],
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should handle "全部" category by not passing category parameter', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: { data: [], total: 0, page: 1, limit: 20, totalPages: 0 },
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
renderHook(() => useWorksSearch({ keyword: '测试', category: '全部' }))
|
|
|
|
// "全部" should be excluded from queryKey
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queryKey: ['worksSearch', '测试', undefined, 1, 20],
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should refetch when category changes', () => {
|
|
const mockData1 = {
|
|
data: [{ id: '1', createdAt: new Date(), template: { id: 't1', name: 'T1' }, status: 'completed', duration: 5 }],
|
|
total: 1,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
}
|
|
|
|
const mockData2 = {
|
|
data: [{ id: '2', createdAt: new Date(), template: { id: 't2', name: 'T2' }, status: 'completed', duration: 10 }],
|
|
total: 1,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
}
|
|
|
|
mockUseQuery
|
|
.mockReturnValueOnce({
|
|
data: mockData1,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
.mockReturnValueOnce({
|
|
data: mockData2,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ keyword, category }: { keyword: string; category?: WorksCategory }) => useWorksSearch({ keyword, category }),
|
|
{
|
|
initialProps: { keyword: '测试', category: '萌宠' },
|
|
}
|
|
)
|
|
|
|
expect(result.current.works).toEqual(mockData1.data)
|
|
|
|
// Switch category
|
|
rerender({ keyword: '测试', category: '写真' })
|
|
|
|
expect(result.current.works).toEqual(mockData2.data)
|
|
})
|
|
})
|
|
|
|
describe('pagination', () => {
|
|
it('should load works with custom page and limit', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: { data: [], total: 0, page: 2, limit: 10, totalPages: 0 },
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() =>
|
|
useWorksSearch({ keyword: '测试', page: 2, limit: 10 })
|
|
)
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queryKey: ['worksSearch', '测试', undefined, 2, 10],
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should use default page 1 and limit 20 when not specified', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: { data: [], total: 0, page: 1, limit: 20, totalPages: 0 },
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
renderHook(() => useWorksSearch({ keyword: '测试' }))
|
|
|
|
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queryKey: ['worksSearch', '测试', undefined, 1, 20],
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('error handling', () => {
|
|
it('should handle API errors', () => {
|
|
const mockError = {
|
|
status: 500,
|
|
statusText: 'Internal Server Error',
|
|
message: 'Failed to search works',
|
|
}
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
error: mockError,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '测试' }))
|
|
|
|
expect(result.current.error).toEqual(mockError)
|
|
expect(result.current.data).toBeUndefined()
|
|
expect(result.current.works).toEqual([])
|
|
})
|
|
|
|
it('should handle network errors', () => {
|
|
const networkError = new Error('Network Error')
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
error: networkError,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '测试' }))
|
|
|
|
expect(result.current.error).toEqual(networkError)
|
|
})
|
|
})
|
|
|
|
describe('loading state', () => {
|
|
it('should set isLoading to true during fetch', () => {
|
|
mockUseQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: true,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '测试' }))
|
|
|
|
expect(result.current.isLoading).toBe(true)
|
|
})
|
|
|
|
it('should set isLoading to false after error', () => {
|
|
const mockError = { status: 500, message: 'Error' }
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
error: mockError,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '测试' }))
|
|
|
|
expect(result.current.isLoading).toBe(false)
|
|
expect(result.current.error).toEqual(mockError)
|
|
})
|
|
})
|
|
|
|
describe('empty results', () => {
|
|
it('should handle empty search results', () => {
|
|
const mockData = {
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 0,
|
|
}
|
|
|
|
mockUseQuery.mockReturnValue({
|
|
data: mockData,
|
|
isLoading: false,
|
|
error: null,
|
|
refetch: mockRefetch,
|
|
})
|
|
|
|
const { result } = renderHook(() => useWorksSearch({ keyword: '不存在的关键词' }))
|
|
|
|
expect(result.current.data).toEqual(mockData)
|
|
expect(result.current.works).toEqual([])
|
|
expect(result.current.error).toBeNull()
|
|
})
|
|
})
|
|
})
|