import { QueryClientProvider } from '@tanstack/react-query' import { act, renderHook, waitFor } from '@testing-library/react-native' import React from 'react' import { buildTestQueryClient } from '../../__utils__/query_client' import type { SearchResultResource } from '../../types/resources/search_result' import { NO_FAILED_TYPES, useSearchResults } from '../use_search_results' const mockGet = jest.fn() jest.mock('../use_api_client', () => ({ useApiClient: () => ({ chat: { get: mockGet } }) })) const result = (type: SearchResultResource['type'], id: string) => ({ type, id, title: `${type} ${id}`, url: `/${type}s/${id}` }) as SearchResultResource const page = (data: SearchResultResource[], meta: Record = {}) => ({ data, links: {}, meta: { count: data.length, totalCount: data.length, ...meta }, }) const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) beforeEach(() => mockGet.mockReset()) describe('useSearchResults', () => { it('flattens pages into one list without repeating a result', async () => { mockGet .mockResolvedValueOnce( page([result('conversation', '1'), result('message', '1')], { next: { offset: '20' } }) ) .mockResolvedValueOnce(page([result('message', '1'), result('message', '2')])) const { result: hook } = renderHook(() => useSearchResults('potluck'), { wrapper }) await waitFor(() => expect(hook.current.hasNextPage).toBe(true)) act(() => { hook.current.loadMore() }) await waitFor(() => expect(hook.current.hasNextPage).toBe(false)) expect(hook.current.results.map(({ type, id }) => `${type}:${id}`)).toEqual([ 'conversation:1', 'message:1', 'message:2', ]) expect(mockGet).toHaveBeenLastCalledWith({ url: '/search_results', data: expect.objectContaining({ q: 'potluck', offset: 20 }), }) }) it('reads failedTypes from the first page', async () => { mockGet .mockResolvedValueOnce(page([], { failedTypes: ['message'], next: { offset: '20' } })) .mockResolvedValueOnce(page([], { failedTypes: ['conversation'] })) const { result: hook } = renderHook(() => useSearchResults('potluck'), { wrapper }) await waitFor(() => expect(hook.current.hasNextPage).toBe(true)) act(() => { hook.current.loadMore() }) await waitFor(() => expect(hook.current.hasNextPage).toBe(false)) expect(hook.current.failedTypes).toEqual(['message']) }) it('makes no request for an empty or whitespace-only term', () => { const { result: hook } = renderHook(() => useSearchResults(' '), { wrapper }) expect(mockGet).not.toHaveBeenCalled() expect(hook.current.results).toEqual([]) expect(hook.current.failedTypes).toBe(NO_FAILED_TYPES) }) it('searches with the trimmed term', async () => { mockGet.mockResolvedValueOnce(page([])) const { result: hook } = renderHook(() => useSearchResults(' potluck '), { wrapper }) await waitFor(() => expect(hook.current.isPending).toBe(false)) expect(mockGet).toHaveBeenCalledWith({ url: '/search_results', data: expect.objectContaining({ q: 'potluck' }), }) }) })