/** * 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() }) }) })